From 4939fab246503cdfd3108013307b9795a0101d90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 15:49:47 +0000 Subject: [PATCH] intake: put the backlog in the Recent feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feed held 50 and could only find 10, because it read every bucket EXCEPT the largest: `draft/` is 150 prompts against a handful of registry rows, so skipping it meant Recent saw almost none of what had been happening. The paging added in #251 had nothing to page. Drafts now carry their own date (PyAutoMind grows a `Filed:` prompt header) and the census reads it back, so a filed prompt is an event like any other. That fills the feed: 45 filed, 3 parked, 1 issued, 1 found on today's Mind, and the `…` reveal finally does something. Epic members stay out, as they do in every pick list on the page — they are worked in order through their epic, and a Recent row hands out a standalone `/start_dev`. `_header_date` replaces the inline `Issued:` lookup: `Issued:` wins over `Filed:` when a prompt carries both, since an issued prompt keeps the `Filed:` it had as a draft and the later event is the one that dates the task. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L4HPWjv5rdzBAkKpfW1SbR --- agents/conductors/intake/AGENTS.md | 6 +++- agents/conductors/intake/_intake.py | 35 ++++++++++++++++++---- tests/test_intake_dashboard.py | 46 +++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/agents/conductors/intake/AGENTS.md b/agents/conductors/intake/AGENTS.md index b9b7ceb..17f5d33 100644 --- a/agents/conductors/intake/AGENTS.md +++ b/agents/conductors/intake/AGENTS.md @@ -90,7 +90,11 @@ schema — light structure over free-form prose. **Recent** is the one section laid out by *date* rather than by state: the 50 newest events on the **work in hand** — issued, parked, filed — merged across -the live buckets and sitting between the Backlog and the Epics. It *holds* 50 +every live bucket (the `draft/` backlog included, which is most of them: +150 prompts against a handful of registry rows) and sitting between the Backlog +and the Epics. Epic members stay out, as they do in every pick list on the +page — they are worked in order through their epic, and a Recent row hands out +a standalone `/start_dev`. It *holds* 50 and *shows* 10 (`RECENT_MAX` / `RECENT_PAGE`): the table is a glance, not a log, so the rest is one tap away — a `…` button on the Pages twin, and nested `
` on the markdown page, which GitHub renders where it strips the diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index 9be5fb0..9d05fdf 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -337,14 +337,15 @@ def parse_header(text: str) -> dict: Only scans the top of the file so a stray "Status:" deep in prose does not fire; first occurrence of each field wins. No YAML — the blessed convention. - `Epic:`/`Phase:` are optional epic-membership fields (dashboard grouping) - and `Issued:` is the prompt's own copy of its registry date; none are in + `Epic:`/`Phase:` are optional epic-membership fields (dashboard grouping); + `Filed:`/`Issued:` are the prompt's own date, keyed by the state it was in + when that happened (PyAutoMind REFERENCE.md "Task dates"). None are in HEADER_FIELDS, so their absence is never header hygiene. """ fields = {} for line in text.splitlines()[:30]: m = re.match(r"(Type|Target|Difficulty|Autonomy|Priority|Status|" - r"Issued|Epic|Phase):\s*(\S.*)", + r"Issued|Filed|Epic|Phase):\s*(\S.*)", line.strip()) if m: fields.setdefault(m.group(1).lower(), m.group(2).strip()) @@ -377,6 +378,18 @@ def _prefix_match(path: str, prefix: str) -> bool: "found", "completed", "shipped") +def _header_date(header: dict) -> str: + """A prompt's own date from its `Issued:` / `Filed:` header, else ''. + + `Issued:` wins when a prompt carries both — it is the later, more specific + event, and an issued prompt keeps the `Filed:` it had as a draft.""" + for key in ("issued", "filed"): + m = _ISO_DATE.search(header.get(key) or "") + if m: + return m.group(1) + return "" + + def _entry_date(fields: dict) -> tuple: """(date, event) for a registry entry, or ('', '') when it carries none.""" for key in DATE_KEYS: @@ -537,6 +550,16 @@ def recent_events(c: dict, limit: int = RECENT_MAX) -> list: events.append({"date": r["date"], "event": r.get("event") or "issued", "title": r["title"], "path": r["path"], "payload": f"/start_dev {r['path']}"}) + # The backlog is the LARGEST pool of work the Mind holds — 150 prompts + # against a handful of live rows — so a feed that skipped it could see + # almost none of what has been happening. Epic members stay out, as they do + # in every pick list on the page: they are worked in order through their + # epic, and a Recent row hands out a standalone `/start_dev`. + for r in c.get("records") or []: + if r.get("date") and not r.get("epic"): + events.append({"date": r["date"], "event": "filed", + "title": r["title"], "path": r["path"], + "payload": f"/start_dev {r['path']}"}) for key, verb in (("parked", "resume"), ("planned", "start")): for e in c.get(key) or []: if e.get("date"): @@ -593,6 +616,9 @@ def census(mind: Path) -> dict: "status": header.get("status", "-"), "epic": header.get("epic", ""), "phase": phase, + # `Filed:` normally; `Issued:` only on a prompt that has been + # issued and moved back, which is still the later event. + "date": _header_date(header), "header": header, "missing": missing, }) @@ -639,8 +665,7 @@ def _count(key): # claims it) dated rather than dropping it out of the recent feed. date, event = row.get("date", ""), row.get("event", "") if not date: - m = _ISO_DATE.search(header.get("issued", "")) - date, event = (m.group(1), "issued") if m else ("", "") + date, event = _header_date(header), "issued" in_flight.append({ "path": rel, "title": _title(text), diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py index de7999d..f75a48e 100644 --- a/tests/test_intake_dashboard.py +++ b/tests/test_intake_dashboard.py @@ -675,3 +675,49 @@ def test_the_html_blurb_renders_its_code_spans(tmp_path): html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80))) assert "complete/index.md" in html assert "`complete/index.md`" not in html + + +# --------------------------------------------------------------------------- # +# recent: the backlog is most of the work +# --------------------------------------------------------------------------- # +def test_a_dated_draft_is_in_the_feed(tmp_path): + """The backlog is the largest pool of work the Mind holds, so a feed that + skipped it saw almost none of what has been happening.""" + mind = _mind(tmp_path, drafts={ + "feature/widgets/sprocket.md": + _prompt("Sprocket work").replace("Status: formalised", + "Status: formalised\nFiled: 2026-08-20")}) + rows = _intake.census(mind)["recent"] + assert [(r["date"], r["event"], r["title"]) for r in rows] == [ + ("2026-08-20", "filed", "Sprocket work")] + assert rows[0]["payload"] == "/start_dev draft/feature/widgets/sprocket.md" + + +def test_an_undated_draft_stays_out(tmp_path): + mind = _mind(tmp_path, drafts={"feature/widgets/sprocket.md": _prompt("S")}) + assert _intake.census(mind)["recent"] == [] + + +def test_an_epic_member_is_not_offered_standalone_in_the_feed(tmp_path): + """Members are worked in order through their epic — every other pick list + on the page excludes them, and a Recent row hands out a `/start_dev`.""" + member = _epic_prompt_body("Phase one", "jax-profiling", phase=1).replace( + "Status: formalised", "Status: formalised\nFiled: 2026-08-20") + mind = _mind(tmp_path, registries={"epics.md": _EPICS}, + drafts={"feature/widgets/phase_one.md": member, + "feature/widgets/loose.md": + _prompt("Loose end").replace( + "Status: formalised", + "Status: formalised\nFiled: 2026-08-21")}) + assert [r["title"] for r in _intake.census(mind)["recent"]] == ["Loose end"] + + +def test_issued_beats_filed_on_a_prompt_carrying_both(tmp_path): + """An issued prompt keeps the `Filed:` it had as a draft; the later, more + specific event is the one the feed reports.""" + body = _prompt("Sprocket").replace( + "Status: formalised", + "Status: formalised\nFiled: 2026-07-01\nIssued: 2026-08-19") + rows = _intake.census( + _mind(tmp_path, active={"sprocket.md": body}))["recent"] + assert [(r["date"], r["event"]) for r in rows] == [("2026-08-19", "issued")]