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
120 changes: 65 additions & 55 deletions agents/conductors/intake/_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,20 @@ def _cell(value: str) -> str:
return str(value).replace("|", "\\|")


def _label(value: str) -> str:
"""Link text for a markdown bullet, made safe to render.

Brackets would end the link early, and a stray `<!--` (some untriaged
prompts open with an HTML comment, which `_title` faithfully reports) would
comment out the rest of the page in GitHub's renderer.
def _summary_label(value: str) -> str:
"""Task text rendered inside a `<summary>` β€” HTML, not markdown.

GitHub does not process markdown inside `<summary>`, so the text is
HTML-escaped and the one markdown idiom Mind titles actually use β€”
`code` spans β€” is translated to `<code>` tags by hand. Comment markers
(some untriaged prompts open with `<!--`, which `_title` faithfully
reports) are stripped rather than escaped: rendered literally they would
just be noise in the row.
"""
value = str(value).replace("<!--", "").replace("-->", "").strip()
return value.replace("[", r"\[").replace("]", r"\]") or "Untitled"
value = (value.replace("&", "&amp;").replace("<", "&lt;")
.replace(">", "&gt;")) or "Untitled"
return re.sub(r"`([^`]+)`", r"<code>\1</code>", value)


# Pick order. The dashboard exists to be *chosen from*, so every list is sorted
Expand All @@ -529,54 +534,55 @@ def _pick_key(r: dict) -> tuple:
r["target"], r["path"])


def _copy_lines(payload: str) -> list:
"""A collapsed copy block nested under a task's bullet.
def _task_row(summary: str, payload: str) -> str:
"""One task as a single collapsed row: `β–Έ πŸ“‹ <task text>`.

The page is read on GitHub (often a phone), where the only clipboard
affordance static markdown can offer is the copy button GitHub renders on
fenced code blocks. So every task carries one, holding the exact message
that routes Claude to the task (`/start_dev <prompt-path>`) β€” tap to
expand, tap to copy, paste into a Claude Code chat. Collapsed behind
`<details>` so each task still reads as one line. The two-space indent
keeps the block inside the bullet's list item; the blank lines around the
fence and after `</details>` are what make GitHub's renderer treat the
fence as markdown and the next bullet as a new list item rather than raw
HTML β€” do not remove them.
fenced code blocks. So every task row is a `<details>` whose summary IS
the task line β€” the πŸ“‹ toggle sits at the left of the text, costing no
extra line and no repeated label β€” and whose hidden body is the fenced
message that routes Claude to the task: tap the row, tap copy, paste into
a Claude Code chat. The blank lines around the fence and after
`</details>` are what make GitHub's renderer treat the fence as markdown
and the next row as a new element rather than raw HTML β€” do not remove
them. The summary is HTML (see `_summary_label`); markdown would not
render there.
"""
return [" <details><summary>πŸ“‹ copy for Claude</summary>",
"",
" ```",
f" {payload}",
" ```",
"",
" </details>"]


def _task_item(head: str, payload: str) -> str:
"""One task β€” the bullet line plus its collapsed copy block."""
return "\n".join([head] + _copy_lines(payload))
return "\n".join([f"<details><summary>πŸ“‹ {summary}</summary>",
"",
"```",
payload,
"```",
"",
"</details>"])


def _items(chunks: list) -> list:
"""Blank-separate multi-line task items so each `</details>` HTML block
ends before the next bullet starts (see `_copy_lines`)."""
"""Blank-separate task rows so each `</details>` HTML block ends before
the next row starts (see `_task_row`)."""
out = []
for chunk in chunks:
out += [chunk, ""]
return out[:-1] if out else []


def _bullet(r: dict) -> str:
"""One backlog prompt as a bullet β€” the phone-readable unit of this page.
"""One backlog prompt as a row β€” the phone-readable unit of this page.

A bullet wraps; a five-column table does not. GitHub's mobile view scrolls
A row wraps; a five-column table does not. GitHub's mobile view scrolls
wide tables sideways, which makes a 133-row backlog unusable on a phone,
so the metadata rides after an em dash instead of in columns.
so the metadata rides after an em dash instead of in columns. The title is
an `<a>` because the row lives in a `<summary>`; its href is repo-root-
relative, which resolves correctly from the page's own blob URL.
"""
facets = " Β· ".join(x for x in (r["target"], r["difficulty"],
r["autonomy"], r["priority"]) if x != "-")
head = f"- [{_label(r['title'])}]({r['path']})" + (f" β€” {facets}" if facets else "")
return _task_item(head, f"/start_dev {r['path']}")
facets = " Β· ".join(_summary_label(x) for x in
(r["target"], r["difficulty"],
r["autonomy"], r["priority"]) if x != "-")
head = f"<a href=\"{r['path']}\">{_summary_label(r['title'])}</a>"
if facets:
head += f" β€” {facets}"
return _task_row(head, f"/start_dev {r['path']}")


def render_dashboard(c: dict) -> str:
Expand All @@ -585,11 +591,12 @@ def render_dashboard(c: dict) -> str:
Tasks only, by design: no readiness verdicts, no test state β€” that is the
Heart's dashboard (`/health`). Two rules shape the layout: it must be
*pickable* (the top of the page answers "what should I do now?", not "how
many prompts are there?"), and it must read on a phone (bullets over wide
tables, long sections behind `<details>`, and a collapsed copy block under
every task so picking one from a phone is copy β†’ paste into a Claude chat,
not retyping a path β€” see `_copy_lines`). Links are repo-root-relative so
they resolve in GitHub's web and mobile markdown views alike.
many prompts are there?"), and it must read on a phone (rows over wide
tables, long sections behind `<details>`, and every task a single
collapsed row whose πŸ“‹ toggle hides its copy block, so picking one from a
phone is copy β†’ paste into a Claude chat, not retyping a path β€” see
`_task_row`). Links are repo-root-relative so they resolve from the
page's GitHub blob URL.
"""
records = sorted(c["records"], key=_pick_key)
L = [
Expand All @@ -600,9 +607,9 @@ def render_dashboard(c: dict) -> str:
"",
"Every task the Mind is holding, on one page: what is in flight, what "
"is parked, and the whole backlog to pick from. Pick a line, then run "
"`/start_dev <prompt-path>` to start it. On a phone, tap **πŸ“‹ copy "
"for Claude** under a task, copy the block, and paste it into a "
"Claude Code chat to route Claude straight to that task.",
"`/start_dev <prompt-path>` to start it. On a phone, tap a task's πŸ“‹ "
"to reveal that command, copy it, and paste it into a Claude Code "
"chat to route Claude straight to the task.",
"",
"Tasks only β€” the organism's health lives with the Heart (`/health`), "
"not here.",
Expand Down Expand Up @@ -639,11 +646,12 @@ def render_dashboard(c: dict) -> str:
"full record for each is in [`active.md`](active.md).", ""]
flight = []
for r in c["in_flight"]:
issue = f" β€” [issue #{r['issue_no']}]({r['issue']})" if r["issue_no"] else ""
status = f" β€” {_clip(r['status'])}" if r["status"] else ""
flight.append(_task_item(
f"- [{_label(r['title'])}]({r['path']}){issue}{status}",
f"/start_dev {r['path']}"))
head = f"<a href=\"{r['path']}\">{_summary_label(r['title'])}</a>"
if r["issue_no"]:
head += f" β€” <a href=\"{r['issue']}\">issue #{r['issue_no']}</a>"
if r["status"]:
head += f" β€” {_summary_label(_clip(r['status']))}"
flight.append(_task_row(head, f"/start_dev {r['path']}"))
L += _items(flight) or ["- _(nothing in flight)_"]
L += [""]

Expand All @@ -661,16 +669,18 @@ def render_dashboard(c: dict) -> str:
"<details>", f"<summary><b>{len(rows)}</b> task(s)</summary>", ""]
items = []
for e in rows:
issue = f" β€” [issue #{e['issue_no']}]({e['issue']})" if e["issue_no"] else ""
status = f" β€” {_clip(e['status'])}" if e["status"] else ""
head = f"<b>{_summary_label(e['slug'])}</b>"
if e["issue_no"]:
head += f" β€” <a href=\"{e['issue']}\">issue #{e['issue_no']}</a>"
if e["status"]:
head += f" β€” {_summary_label(_clip(e['status']))}"
# A registry row may name its prompt file; without one there is no
# start_dev target, so route the slug as free prose instead.
prompt = (e["prompt"].split() or [""])[0]
payload = (f"/start_dev {prompt}" if prompt.endswith(".md") else
f"/route {verb} the {key} PyAutoMind task "
f"{e['slug']} β€” its record is in {key}.md")
items.append(_task_item(
f"- **{_label(e['slug'])}**{issue}{status}", payload))
items.append(_task_row(head, payload))
L += _items(items) or ["- _(none)_"]
L += ["", "</details>", ""]

Expand Down
7 changes: 4 additions & 3 deletions skills/intake/intake.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ dev workflow (issue, branch, plan). Do not bypass the Brain.
- `bin/pyauto-brain intake dashboard` β€” renders the census as the Mind **task**
page (`PyAutoMind/dashboard.md`, linked from that repo's README): the picks
worth starting now, then in flight / parked / planned / the whole backlog.
Every task carries a collapsed **πŸ“‹ copy for Claude** block (GitHub's
code-fence copy button) holding the `/start_dev <prompt-path>` message that
routes Claude to that task β€” the phone path from the page into a session.
Every task renders as one collapsed row whose leading πŸ“‹ toggle hides a
code fence (GitHub's copy button) holding the `/start_dev <prompt-path>`
message that routes Claude to that task β€” the phone path from the page
into a session.
Dry-run prints it, `--apply` writes it (commit via `prompt_sync_push`),
`--check` exits 1 if the committed page has drifted β€” PyAutoMind's
`dashboard_refresh.yml` self-heals that on pushes to main, so regenerate it
Expand Down
68 changes: 42 additions & 26 deletions tests/test_intake_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,18 @@ def test_quick_wins_are_small_and_safe_only(tmp_path):
assert "Safe but large" not in quick


def test_every_backlog_prompt_is_a_bullet_not_a_wide_table_row(tmp_path):
"""Wide tables scroll sideways on a phone; bullets wrap. Pin the shape."""
def test_every_backlog_prompt_is_one_collapsed_row_not_a_wide_table(tmp_path):
"""Wide tables scroll sideways on a phone; rows wrap. Pin the shape: one
`<details>` per task whose summary is `πŸ“‹ <linked title> β€” facets`."""
mind = _mind(tmp_path, drafts={
"bug/widgets/one.md": _prompt("Bug one"),
"feature/widgets/two.md": _prompt("Feature two"),
})
page = _page(mind)
backlog = page.split("## Backlog")[1]
assert "- [Bug one](draft/bug/widgets/one.md)" in backlog
assert "- [Feature two](draft/feature/widgets/two.md)" in backlog
assert ('<details><summary>πŸ“‹ <a href="draft/bug/widgets/one.md">'
"Bug one</a> β€” ") in backlog
assert '<a href="draft/feature/widgets/two.md">Feature two</a>' in backlog
# The only table on the page is the 2-column where/count summary.
assert backlog.count("|") == 0, "the backlog must not render as tables"
assert "<summary><b>bug</b> β€” 1</summary>" in backlog, \
Expand Down Expand Up @@ -122,7 +124,8 @@ def test_in_flight_links_the_registry_issue_and_its_live_status(tmp_path):
active={"widget_rework.md": _prompt("Widget rework")},
registries={"active.md": ACTIVE_MD})
flight = _page(mind).split("## In flight")[1].split("## Parked")[0]
assert "[issue #42](https://github.com/ExampleOrg/Widgets/issues/42)" in flight, \
assert ('<a href="https://github.com/ExampleOrg/Widgets/issues/42">'
"issue #42</a>") in flight, \
"the link must be the matched URL, not the field's trailing prose"
assert "(opened after the spike)" not in flight
assert "library-dev" in flight
Expand Down Expand Up @@ -170,33 +173,42 @@ def test_in_flight_prompt_with_no_registry_row_claims_no_issue(tmp_path):
def test_registry_entry_without_fields_still_lists(tmp_path):
mind = _mind(tmp_path, registries={"parked.md": "# Parked\n\n## lonely-slug\n"})
parked = _page(mind).split("## Parked")[1].split("## Planned")[0]
assert "**lonely-slug**" in parked
assert "<b>lonely-slug</b>" in parked


# --------------------------------------------------------------------------- #
# copy blocks: every task carries a paste-ready message that routes Claude
# copy blocks: every task row's πŸ“‹ toggle hides a paste-ready message
# --------------------------------------------------------------------------- #
COPY_SUMMARY = "<summary>πŸ“‹ copy for Claude</summary>"


def test_backlog_and_picks_carry_a_start_dev_copy_block(tmp_path):
"""GitHub's copy button lives on fenced code blocks β€” the one clipboard a
static page has, and the whole point of the block on a phone."""
static page has, and the whole point of the row's hidden body on a phone."""
mind = _mind(tmp_path, drafts={
"bug/widgets/one.md": _prompt("Bug one", priority="high")})
page = _page(mind)
fence = " ```\n /start_dev draft/bug/widgets/one.md\n ```"
fence = "```\n/start_dev draft/bug/widgets/one.md\n```"
head, backlog = page.split("## In flight")[0], page.split("## Backlog")[1]
assert fence in head and COPY_SUMMARY in head, \
assert fence in head and "<details><summary>πŸ“‹ " in head, \
"the Start-here picks must carry the copy block"
assert fence in backlog and COPY_SUMMARY in backlog, \
"backlog bullets must carry the copy block"
assert fence in backlog and "<details><summary>πŸ“‹ " in backlog, \
"backlog rows must carry the copy block"


def test_task_row_is_one_line_with_no_repeated_label(tmp_path):
"""The πŸ“‹ toggle rides at the left of the task text on the SAME line β€”
an extra 'copy for Claude' line per task doubled the page's height."""
mind = _mind(tmp_path, drafts={
"bug/widgets/one.md": _prompt("Bug one", priority="high")})
page = _page(mind)
assert "copy for Claude" not in page
assert ('<details><summary>πŸ“‹ <a href="draft/bug/widgets/one.md">'
"Bug one</a>") in page, \
"the summary line must open with πŸ“‹ then the task text"


def test_in_flight_copy_block_targets_the_active_prompt(tmp_path):
mind = _mind(tmp_path, active={"widget_rework.md": _prompt("Widget rework")})
flight = _page(mind).split("## In flight")[1].split("## Parked")[0]
assert " /start_dev active/widget_rework.md" in flight
assert "\n/start_dev active/widget_rework.md\n" in flight


def test_registry_row_copy_block_prefers_its_prompt_path(tmp_path):
Expand All @@ -206,21 +218,21 @@ def test_registry_row_copy_block_prefers_its_prompt_path(tmp_path):
"# Parked\n\n## with-prompt\n- prompt: active/widget_rework.md\n"
"\n## lonely-slug\n")})
parked = _page(mind).split("## Parked")[1].split("## Planned")[0]
assert " /start_dev active/widget_rework.md" in parked
assert (" /route resume the parked PyAutoMind task lonely-slug β€” "
"its record is in parked.md") in parked
assert "\n/start_dev active/widget_rework.md\n" in parked
assert ("\n/route resume the parked PyAutoMind task lonely-slug β€” "
"its record is in parked.md\n") in parked


def test_copy_details_never_swallow_the_next_bullet(tmp_path):
def test_copy_details_never_swallow_the_next_row(tmp_path):
"""GitHub's renderer treats lines after `</details>` as raw HTML until a
blank line β€” a bullet directly beneath one would vanish from the page."""
blank line β€” a row directly beneath one would vanish from the page."""
mind = _mind(tmp_path, drafts={
"bug/widgets/one.md": _prompt("Bug one"),
"bug/widgets/two.md": _prompt("Bug two"),
}, active={"a.md": _prompt("A"), "b.md": _prompt("B")})
page = _page(mind)
assert "</details>\n<details>" not in page
assert "</details>\n-" not in page
assert "</details>\n <details>" not in page


# --------------------------------------------------------------------------- #
Expand All @@ -237,12 +249,16 @@ def test_a_prompt_titled_with_an_html_comment_cannot_swallow_the_page(tmp_path):
assert "Visible after the comment" in page


def test_bracketed_title_does_not_break_the_link(tmp_path):
def test_title_markup_survives_the_html_summary(tmp_path):
"""Summaries are HTML: brackets pass through untouched, raw angle brackets
are escaped, and a title's `code` span renders as <code> (GitHub does not
process markdown inside <summary>)."""
mind = _mind(tmp_path, drafts={
"bug/widgets/b.md": _prompt("[JAX] fails on [gpu]", priority="high")})
"bug/widgets/b.md": _prompt("[JAX] `grad(x)` fails on x<0",
priority="high")})
page = _page(mind)
assert r"\[JAX\] fails on \[gpu\]" in page
assert "(draft/bug/widgets/b.md)" in page
assert ('<a href="draft/bug/widgets/b.md">'
"[JAX] <code>grad(x)</code> fails on x&lt;0</a>") in page


# --------------------------------------------------------------------------- #
Expand Down
Loading