Skip to content

Commit 860dd03

Browse files
Jammy2211Jammy2211claude
authored
intake dashboard: Epics section under In flight + per-section markdown-version links (PyAutoMind#251) (#242)
Co-authored-by: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent db7715b commit 860dd03

2 files changed

Lines changed: 143 additions & 3 deletions

File tree

agents/conductors/intake/_intake.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,64 @@ def parse_registry(path: Path) -> list:
389389
return entries
390390

391391

392+
# `epics.md` — the Mind's registry of long-running multi-phase programmes.
393+
# Same H2-slug + `- key: value` shape as active.md, but the fields differ:
394+
# `ledger:` names the epic's canonical state file (may live in another repo),
395+
# `title:`/`status:`/`notes:` are display prose. The dashboard's job is only
396+
# to hand a session enough to WORK OUT where the epic stands — the ledger
397+
# stays the single source of truth.
398+
_EPIC_FIELDS = ("title", "ledger", "status", "notes")
399+
400+
401+
def parse_epics(path: Path) -> list:
402+
"""Parse `epics.md` into `[{slug, title, ledger, status, notes}]`.
403+
404+
Tolerant like parse_registry: a slug alone still yields a record; absent
405+
file -> empty list (a freshly-spawned Mind has no epics).
406+
"""
407+
if not path.is_file():
408+
return []
409+
entries, cur = [], None
410+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
411+
head = _REG_HEAD.match(line)
412+
if head:
413+
cur = {"slug": head.group(1)}
414+
cur.update({k: "" for k in _EPIC_FIELDS})
415+
entries.append(cur)
416+
continue
417+
if cur is None:
418+
continue
419+
field = _REG_FIELD.match(line)
420+
if field and field.group(1) in _EPIC_FIELDS and not cur[field.group(1)]:
421+
cur[field.group(1)] = field.group(2).strip()
422+
return entries
423+
424+
425+
def _epic_prompt(e: dict) -> str:
426+
"""The one-tap resume prompt: work out where the epic is, then continue.
427+
428+
Deliberately a procedure, not a snapshot — any phase/issue state baked in
429+
here would go stale the moment the epic advances, which is exactly the
430+
problem the button exists to solve."""
431+
name = e.get("title") or e.get("slug", "this")
432+
ledger = e.get("ledger", "")
433+
parts = [f"Continue the '{name}' epic."]
434+
if ledger:
435+
parts.append(
436+
f"Its canonical state lives in {ledger} — read that ledger (and "
437+
"any DECISIONS/RESULTS files beside it) first.")
438+
parts.append(
439+
"Cross-check this epic's entry in PyAutoMind/epics.md, any related "
440+
"rows in PyAutoMind/active.md, and the referenced repos' open issues "
441+
"and PRs, to work out the last completed phase and what is currently "
442+
"in flight. Then pick the next logical step and continue it through "
443+
"the normal workflow (/start_dev — filing the phase's prompt first "
444+
"if none exists), updating the ledger as the work advances.")
445+
if e.get("notes"):
446+
parts.append(f"Note: {e['notes']}")
447+
return " ".join(parts)
448+
449+
392450
def _clip(text: str, limit: int = 130) -> str:
393451
"""First line of a registry value, clipped at a word boundary."""
394452
text = text.strip().splitlines()[0].strip() if text.strip() else ""
@@ -492,6 +550,7 @@ def _count(key):
492550
"by_priority": _count("priority"),
493551
"records": records,
494552
"in_flight": in_flight,
553+
"epics": parse_epics(mind / "epics.md"),
495554
"parked": parked,
496555
"planned": planned,
497556
"hygiene": hygiene,
@@ -687,6 +746,23 @@ def render_dashboard(c: dict) -> str:
687746
L += _items(flight) or ["- _(nothing in flight)_"]
688747
L += [""]
689748

749+
if c.get("epics"):
750+
L += ["## Epics", "",
751+
"Long-running multi-phase programmes. Each 📋 prompt has Claude "
752+
"read the epic's ledger, work out where it stands, and continue "
753+
"from the next logical point — no hunting for the paired issue. "
754+
"Full record in [`epics.md`](epics.md).", ""]
755+
items = []
756+
for e in c["epics"]:
757+
head = f"<b>{_summary_label(e.get('title') or e['slug'])}</b>"
758+
if e.get("ledger"):
759+
head += f" — ledger: `{e['ledger']}`"
760+
if e.get("status"):
761+
head += f" — {_summary_label(_clip(e['status']))}"
762+
items.append(_task_row(head, _epic_prompt(e)))
763+
L += _items(items)
764+
L += [""]
765+
690766
for key, heading, verb, blurb in (
691767
("parked", "Parked", "resume",
692768
"Started or scoped, not currently in flight — "
@@ -855,7 +931,14 @@ def record_row(r):
855931
H += [f'<h3>{title} <span class="facets">({note}){more}</span></h3>']
856932
H += [record_row(r) for r in shown] or ['<p class="muted">(none right now)</p>']
857933

858-
H += ["<h2>In flight</h2>",
934+
def h2(title, src):
935+
# Every section links the markdown file it is rendered from — the
936+
# registry file is the full record, the page is the view.
937+
a = (f' <a class="facets" href="{_attr(blob + src)}">markdown '
938+
"version</a>") if blob else ""
939+
return f"<h2>{title}{a}</h2>"
940+
941+
H += [h2("In flight", "active.md"),
859942
'<p class="muted">Issued — each has an open GitHub issue and '
860943
"usually a branch.</p>"]
861944
for r in c["in_flight"]:
@@ -869,10 +952,25 @@ def record_row(r):
869952
if not c["in_flight"]:
870953
H.append('<p class="muted">(nothing in flight)</p>')
871954

955+
if c.get("epics"):
956+
H += [h2("Epics", "epics.md"),
957+
'<p class="muted">Long-running multi-phase programmes — 📋 '
958+
"copies a prompt that works out where the epic stands from its "
959+
"ledger and continues it from the next logical point.</p>"]
960+
for e in c["epics"]:
961+
text = f"<b>{_summary_label(e.get('title') or e['slug'])}</b>"
962+
if e.get("ledger"):
963+
text += (f' — <span class="facets">ledger: '
964+
f"<code>{_attr(e['ledger'])}</code></span>")
965+
if e.get("status"):
966+
text += (f' — <span class="facets">'
967+
f'{_summary_label(_clip(e["status"]))}</span>')
968+
H.append(_html_task(text, _epic_prompt(e)))
969+
872970
for key, heading, verb in (("parked", "Parked", "resume"),
873971
("planned", "Planned", "start")):
874972
rows = c[key]
875-
H += [f"<h2>{heading}</h2>", "<details>",
973+
H += [h2(heading, f"{key}.md"), "<details>",
876974
f"<summary>{len(rows)} task(s)</summary>"]
877975
for e in rows:
878976
text = f"<b>{_summary_label(e['slug'])}</b>"
@@ -887,7 +985,7 @@ def record_row(r):
887985
H.append('<p class="muted">(none)</p>')
888986
H.append("</details>")
889987

890-
H += ["<h2>Backlog</h2>",
988+
H += [h2("Backlog", "draft").replace("/blob/main/draft", "/tree/main/draft"),
891989
f'<p class="muted">{c["total"]} filed prompts, not started — '
892990
"sorted most-pickable first (priority, then size).</p>"]
893991
for wt, n in c["by_work_type"].items():

tests/test_intake_dashboard.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,45 @@ def test_check_ignores_the_generation_stamp_but_sees_content_drift(tmp_path, cap
334334
def test_check_on_a_missing_dashboard_is_drift(tmp_path):
335335
mind = _mind(tmp_path, drafts={"bug/widgets/one.md": _prompt("Bug one")})
336336
assert _intake.main(["--mind", str(mind), "dashboard", "--check"]) == 1
337+
338+
339+
# --------------------------------------------------------------------------- #
340+
# epics: long-running programmes resume from their ledger, not a paired issue
341+
# --------------------------------------------------------------------------- #
342+
_EPICS = """# Epics
343+
344+
## jax-profiling
345+
- title: JAX inference programme
346+
- ledger: autolens_profiling/results/notes/inference/PROGRAMME.md
347+
- notes: slices ship as autolens_profiling issues/PRs, not Mind prompts
348+
349+
## bare-epic
350+
"""
351+
352+
353+
def test_epics_section_sits_under_in_flight_with_a_resume_prompt(tmp_path):
354+
mind = _mind(tmp_path, registries={"epics.md": _EPICS})
355+
page = _page(mind)
356+
assert page.index("## In flight") < page.index("## Epics") < page.index("## Parked")
357+
epics = page.split("## Epics")[1].split("## Parked")[0]
358+
assert "JAX inference programme" in epics
359+
assert "PROGRAMME.md" in epics
360+
# The copy payload is a procedure — work out the state, then continue.
361+
assert "work out the last completed phase" in epics
362+
assert "/start_dev" in epics
363+
# A slug-only entry still lists (tolerant, like the other registries).
364+
assert "bare-epic" in epics
365+
366+
367+
def test_no_epics_file_means_no_epics_section(tmp_path):
368+
page = _page(_mind(tmp_path, active={"one.md": _prompt("Solo task")}))
369+
assert "## Epics" not in page, "a spawned Mind without epics.md stays clean"
370+
371+
372+
def test_html_sections_link_their_markdown_source(tmp_path):
373+
mind = _mind(tmp_path, registries={"epics.md": _EPICS,
374+
"repos.yaml": REPOS_YAML})
375+
html = _html(mind)
376+
for src in ("active.md", "epics.md", "parked.md", "planned.md"):
377+
assert f'/blob/main/{src}">markdown version</a>' in html, src
378+
assert '/tree/main/draft">markdown version</a>' in html

0 commit comments

Comments
 (0)