diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py
index a4eb185..1138344 100755
--- a/agents/conductors/intake/_intake.py
+++ b/agents/conductors/intake/_intake.py
@@ -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 `", "").strip()
- return value.replace("[", r"\[").replace("]", r"\]") or "Untitled"
+ value = (value.replace("&", "&").replace("<", "<")
+ .replace(">", ">")) or "Untitled"
+ return re.sub(r"`([^`]+)`", r"\1", value)
# Pick order. The dashboard exists to be *chosen from*, so every list is sorted
@@ -529,37 +534,33 @@ 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: `โธ ๐ `.
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 `) โ tap to
- expand, tap to copy, paste into a Claude Code chat. Collapsed behind
- `` 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 ` ` 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 `` 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
+ ` ` 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 [" ๐ copy for Claude
",
- "",
- " ```",
- f" {payload}",
- " ```",
- "",
- " "]
-
-
-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"๐ {summary}
",
+ "",
+ "```",
+ payload,
+ "```",
+ "",
+ " "])
def _items(chunks: list) -> list:
- """Blank-separate multi-line task items so each `` HTML block
- ends before the next bullet starts (see `_copy_lines`)."""
+ """Blank-separate task rows so each `` HTML block ends before
+ the next row starts (see `_task_row`)."""
out = []
for chunk in chunks:
out += [chunk, ""]
@@ -567,16 +568,21 @@ def _items(chunks: list) -> list:
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 `` because the row lives in a ``; 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"{_summary_label(r['title'])}"
+ if facets:
+ head += f" โ {facets}"
+ return _task_row(head, f"/start_dev {r['path']}")
def render_dashboard(c: dict) -> str:
@@ -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 ``, 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 ``, 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 = [
@@ -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 ` 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 ` 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.",
@@ -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"{_summary_label(r['title'])}"
+ if r["issue_no"]:
+ head += f" โ issue #{r['issue_no']}"
+ 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 += [""]
@@ -661,16 +669,18 @@ def render_dashboard(c: dict) -> str:
"", f"{len(rows)} task(s)
", ""]
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"{_summary_label(e['slug'])}"
+ if e["issue_no"]:
+ head += f" โ issue #{e['issue_no']}"
+ 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 += ["", " ", ""]
diff --git a/skills/intake/intake.md b/skills/intake/intake.md
index 8a7184a..5182f86 100644
--- a/skills/intake/intake.md
+++ b/skills/intake/intake.md
@@ -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 ` 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 `
+ 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
diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py
index c359d63..79a25dd 100644
--- a/tests/test_intake_dashboard.py
+++ b/tests/test_intake_dashboard.py
@@ -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
+ `` per task whose summary is `๐ โ 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 ('๐ '
+ "Bug one โ ") in backlog
+ assert 'Feature two' 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 "bug โ 1
" in backlog, \
@@ -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 (''
+ "issue #42") 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
@@ -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 "lonely-slug" 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 = "๐ copy for Claude
"
-
-
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 "๐ " 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 "๐ " 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 ('๐ '
+ "Bug one") 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):
@@ -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 `
` 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 "
\n" not in page
assert " \n-" not in page
- assert "
\n " not in page
# --------------------------------------------------------------------------- #
@@ -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 (GitHub does not
+ process markdown inside )."""
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 (''
+ "[JAX] grad(x) fails on x<0") in page
# --------------------------------------------------------------------------- #