Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion agents/conductors/intake/_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,14 @@ def write_prompt(mind: Path, decision: dict, body_text: str, source_note: str):
_FIX_PR_RE = re.compile(r"^Fix:.*(?:PR\s*#\d+|/pull/\d+)",
re.MULTILINE | re.IGNORECASE)

# The other half of the same failure: a session finishes the work, writes the
# outcome into the prompt's own `Status:` header — `shipped`, `superseded`,
# `absorbed` — and then leaves the file in `draft/`, where it keeps rendering as
# pickable backlog. Read off the parsed header (not the body) so a prompt that
# merely *describes* shipped sibling work is never flagged.
DONE_STATUSES = ("shipped", "superseded", "absorbed", "complete", "completed",
"done", "retired")


def parse_header(text: str) -> dict:
"""Extract the light metadata header (`Field: value` lines) from a prompt.
Expand All @@ -400,6 +408,19 @@ def parse_header(text: str) -> dict:
return fields


def _done_status(header: dict) -> str:
"""The first word of a `Status:` header that declares the work finished.

Returns `""` for a status that is merely *about* finished work (`phases 1-3
SHIPPED; phase 4 open`, `split (phases 1-2 SHIPPED; phase 3 open)`): only a
status that *opens* on a done-word means the prompt itself is spent. Empty
string is falsey, so callers read as a plain condition.
"""
first = header.get("status", "").strip().lower().lstrip("*_`").split()
return first[0].rstrip(":;,.") if first and \
first[0].rstrip(":;,.") in DONE_STATUSES else ""


def _prefix_match(path: str, prefix: str) -> bool:
"""Match a census path against a user prefix, with or without `draft/`."""
sans = path[len("draft/"):] if path.startswith("draft/") else path
Expand Down Expand Up @@ -675,6 +696,10 @@ def census(mind: Path) -> dict:
if _FIX_PR_RE.search(text):
drift.append(f"{rel} — body records a fix PR, but the prompt "
"never left draft/ (reconcile its lifecycle)")
elif _done_status(header):
drift.append(f"{rel} — its own `Status:` says "
f"{_done_status(header)}, but the prompt never "
"left draft/ (reconcile its lifecycle)")

def _count(key):
out = {}
Expand Down Expand Up @@ -829,6 +854,50 @@ def _pick_key(r: dict) -> tuple:
r["target"], r["path"])


# --- freshness banner ----------------------------------------------------------
# The page is only as current as the files it is generated from, and the way it
# goes wrong is asymmetric: `dashboard_refresh.yml` self-heals a stale *render*
# on every push to main, but nothing self-heals a stale *prompt* — a task that
# shipped without its prompt advancing to complete/ keeps rendering as pickable
# backlog until a human reconciles it. So the banner states the generation date
# and hands over the whole reconcile-then-regenerate chore as one copyable
# message, in the same 📋 idiom as every task row.
REFRESH_PAYLOAD = """\
Bring the PyAutoMind dashboard up to date. Work in the PyAutoMind checkout:

1. `git fetch origin && git status`. If behind `origin/main`, `git pull --ff-only`
before touching anything.
2. `python3 scripts/lifecycle.py check`, `orphans`, and `index --check`. Fix
whatever drift they report.
3. Reconcile finished work — this is the part nothing automates. For every prompt
under `draft/` and `active/`, decide whether it is already done: a `Status:`
header saying shipped/superseded/absorbed, a merged PR named in its body, or a
record in `complete/` whose scope already covers it (check `complete/index.md`
and grep the dated buckets). Treat a same-subject record as evidence, not
proof — read both and confirm the scope really matches before retiring a
prompt.
4. For each one that IS done, write its record and retire the prompt:
`python3 scripts/lifecycle.py record <slug> --date <YYYY-MM-DD> --from-file
<body> --apply`, where <body> ends with `## Original prompt` followed by the
prompt's full text. Then `git rm` the prompt file and repoint every
cross-reference to it (grep the slug across `draft/`, `active/`, `epics.md`
and the registry files).
5. Regenerate the page: `pyauto-brain intake --apply dashboard`. Never hand-edit
`dashboard.md` or `dashboard.html` — they are generated.
6. Commit and push to `main`, so `dashboard_refresh.yml` agrees with the tree.

Report what you retired, what you deliberately left in the backlog and why, and
anything you could not verify."""

REFRESH_BLURB = (
"generated from `active/`, `draft/` and the registry files, so it is only "
"as current as they are. `dashboard_refresh.yml` re-renders it on every "
"push to `main` — that heals a stale page, but not a stale prompt: a task "
"that shipped without its prompt advancing to `complete/` keeps rendering "
"here as pickable backlog. Reconciling those is the refresh below."
)


def _task_row(summary: str, payload: str) -> str:
"""One task as a single collapsed row: `▸ 📋 <task text>`.

Expand Down Expand Up @@ -1021,6 +1090,11 @@ def render_dashboard(c: dict) -> str:
"[Recent](#recent) is the same work by date — what has been happening "
"rather than what to do next.",
"",
f"> **Last updated {c['generated']}.** This page is {REFRESH_BLURB}",
"",
_task_row("<b>Refresh this page</b> — reconcile finished prompts, "
"then regenerate", REFRESH_PAYLOAD),
"",
"| Where | Count |",
"|-------|------:|",
f"| [In flight](#in-flight) (`active/`) | {c['issued_count']} |",
Expand Down Expand Up @@ -1172,6 +1246,15 @@ def render_dashboard(c: dict) -> str:
# The dashboard-only half of the page script: the shared clipboard
# handler lives in the board theme, this reveals the Recent feed a page
# at a time.
# The freshness banner is a dashboard-only element (no other organ board carries
# one), so its rule lives here rather than in the shared theme.
_FRESH_CSS = """\
.fresh{margin:0 0 1.2rem;padding:.7rem .9rem .4rem;border:1px solid var(--edge);
border-radius:11px;background:var(--tint)}
.fresh>p{margin:0 0 .3rem}
.fresh .task{border-bottom:0}
"""

_MORE_JS = """\
// Recent shows one page and reveals the next on each tap of the \u2026 button,
// which retires itself once the feed is exhausted. Every row is already in the
Expand All @@ -1188,6 +1271,17 @@ def render_dashboard(c: dict) -> str:
"""


def _md_inline(text: str) -> str:
"""Render the inline markdown this module authors (`code` spans only) as HTML.

The freshness blurb is written once and rendered on both pages; the markdown
page takes it verbatim, this turns its backticks into `<code>` so the HTML
twin does not print them literally. Deliberately not a markdown parser —
it handles exactly the one construct the blurb uses.
"""
return re.sub(r"`([^`]+)`", r"<code>\1</code>", text)


def _attr(value: str) -> str:
"""Escape a string for a double-quoted HTML attribute."""
return (str(value).replace("&", "&amp;").replace('"', "&quot;")
Expand Down Expand Up @@ -1237,7 +1331,7 @@ def record_row(r):
"<title>PyAutoMind Dashboard</title>",
f"<!-- generated by `pyauto-brain intake dashboard --apply` on "
f"{c['generated']} — regenerate, do not hand-edit -->",
f"<style>{_theme_css(THEME_ORGAN)}</style>",
f"<style>{_theme_css(THEME_ORGAN)}{_FRESH_CSS}</style>",
"</head>",
"<body>",
hero(THEME_ORGAN, "Dashboard",
Expand All @@ -1250,6 +1344,11 @@ def record_row(r):
stats((c["issued_count"], "In flight"), (len(c["parked"]), "Parked"),
(len(c["planned"]), "Planned"), (c["total"], "Backlog")),
]
H += [f'<div class="fresh"><p><b>Last updated {c["generated"]}.</b> '
f'This page is {_md_inline(REFRESH_BLURB)}</p>',
_html_task("<b>Refresh this page</b> — reconcile finished prompts, "
"then regenerate", REFRESH_PAYLOAD),
"</div>"]
if home:
H.append(f'<p class="muted mdsrc">'
f'{link("dashboard.md", "markdown version")}</p>')
Expand Down
81 changes: 80 additions & 1 deletion tests/test_intake_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import importlib.util
import re
import sys
from pathlib import Path

Expand Down Expand Up @@ -480,6 +481,74 @@ def test_a_prompt_merely_citing_a_pr_is_not_drift(tmp_path):
assert "Needs lifecycle reconciliation" not in _page(mind)


def test_a_draft_whose_own_status_says_shipped_is_flagged(tmp_path):
"""The commonest way a finished task keeps advertising itself as backlog:
the shipping session writes the outcome into the prompt's `Status:` header
and leaves the file in `draft/`."""
mind = _mind(tmp_path, drafts={
"bug/widgets/done.md": _prompt("Already fixed",
status="shipped 2026-08-24 (#277)")})
page = _page(mind)
assert "Needs lifecycle reconciliation" in page
head = page.split("## Start here")[0]
assert "bug/widgets/done.md" in head and "shipped" in head


def test_superseded_and_absorbed_statuses_are_drift_too(tmp_path):
for status in ("superseded by the epic", "ABSORBED 2026-08-10", "retired"):
mind = _mind(tmp_path / status.split()[0],
drafts={"bug/widgets/x.md": _prompt("Spent", status=status)})
assert "Needs lifecycle reconciliation" in _page(mind), status


def test_a_partly_shipped_status_is_not_drift(tmp_path):
"""A tracker reporting *some* phases shipped is still live work — only a
status that OPENS on a done-word means the prompt itself is spent."""
for status in ("phases 1-3 SHIPPED; phase 4 open",
"split (phases 1-2 SHIPPED 2026-08-23; phase 3 open)",
"in progress — core landed, real-data swap-in remains"):
mind = _mind(tmp_path / status.split()[0],
drafts={"bug/widgets/x.md": _prompt("Live", status=status)})
assert "Needs lifecycle reconciliation" not in _page(mind), status


# --------------------------------------------------------------------------- #
# freshness: the page says how current it is, and hands over its own refresh
# --------------------------------------------------------------------------- #
def test_the_page_states_when_it_was_generated_and_why_that_can_lie(tmp_path):
mind = _mind(tmp_path, drafts={"bug/widgets/x.md": _prompt("A task")})
c = _intake.census(mind)
page = _intake.render_dashboard(c)
banner = page.split("| Where | Count |")[0]
assert f"Last updated {c['generated']}" in banner
# the distinction that matters: a self-healing render is not a fresh backlog
assert "dashboard_refresh.yml" in banner and "stale prompt" in banner


def test_the_refresh_banner_is_a_copyable_instruction_not_a_bare_command(tmp_path):
mind = _mind(tmp_path, drafts={"bug/widgets/x.md": _prompt("A task")})
banner = _page(mind).split("| Where | Count |")[0]
assert "📋" in banner, "the banner uses the same one-tap idiom as a task row"
for step in ("lifecycle.py record", "git pull --ff-only",
"pyauto-brain intake --apply dashboard"):
assert step in banner, step
assert "never hand-edit" in banner.lower()


def test_the_html_twin_carries_the_banner_with_a_real_copy_button(tmp_path):
mind = _mind(tmp_path, drafts={"bug/widgets/x.md": _prompt("A task")})
c = _intake.census(mind)
html = _intake.render_dashboard_html(c)
fresh = html.split('<div class="fresh">')[1].split("</div>")[0]
assert f"Last updated {c['generated']}" in fresh
assert 'button class="copy"' in fresh and "data-cmd=" in fresh
# backticks are markdown; the blurb must render them as code spans, while
# the copy payload keeps its own verbatim (see _prose)
assert "<code>draft/</code>" in _prose(fresh) and "`draft/`" not in _prose(fresh)
assert "`git pull --ff-only`" in fresh, "the payload is copied, not rendered"
assert ".fresh{" in html, "the banner ships its own rule, not the shared theme"


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"
Expand Down Expand Up @@ -704,10 +773,20 @@ def test_html_ships_every_row_so_a_reader_without_js_sees_the_feed(tmp_path):
assert section.count("<tr") == 50


def _prose(html: str) -> str:
"""The page minus every copy payload.

A `data-cmd` attribute is a clipboard literal — the message a human pastes
into a Claude chat — so it legitimately carries markdown that the page must
NOT render. Assertions about how the page *reads* have to exclude it.
"""
return re.sub(r'data-cmd="[^"]*"', "data-cmd=\"\"", html)


def test_the_html_blurb_renders_its_code_spans(tmp_path):
"""The blurb is shared with the markdown page; its backticks would
otherwise print literally here."""
html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80)))
html = _prose(_intake.render_dashboard_html(_intake.census(_many(tmp_path, 80))))
assert "<code>complete/index.md</code>" in html
assert "`complete/index.md`" not in html

Expand Down
Loading