feat(kanban): surface task_runs.summary on dashboard cards + kanban show#20251
feat(kanban): surface task_runs.summary on dashboard cards + kanban show#20251Brecht-H wants to merge 1 commit into
Conversation
… show``
The kanban-worker skill (built into the gateway dispatcher's spawn
prompt) instructs every worker to hand off via
``kanban_complete(summary=..., metadata=...)``. That writes the summary
onto the closing ``task_runs`` row, NOT onto ``tasks.result`` — the
latter is left NULL unless the caller passes ``result=`` explicitly.
Result: a glance at the dashboard or ``hermes kanban show <id>`` shows
a blank "Result:" section even when the worker did real work, which
on 2026-05-05 caused a Mac false-alarm ("Hermes did nothing") on a
task that had a 10-line completion summary on its run.
This patch surfaces the latest non-null run summary as
``latest_summary`` so the worker's actual handoff lands in front of
operators.
* New helpers ``kanban_db.latest_summary(conn, task_id)`` and
``kanban_db.latest_summaries(conn, task_ids)``. The batch variant
uses a single window-function SELECT so the dashboard board endpoint
doesn't pay an N+1 cost on multi-hundred-task boards.
* CLI ``hermes kanban show <id>`` prints a "Latest summary:" block
when ``tasks.result`` is empty but a run has produced a summary
(the existing "Result:" section still wins when populated, so the
back-compat path for hand-edited results is untouched). JSON output
gains a top-level ``latest_summary`` field.
* Dashboard ``/board`` and ``/tasks/{id}`` now include a
``latest_summary`` field on every task. Cards on /board carry a
200-character preview (cheap to render, plenty for "what did this
worker do?" at a glance); the drawer/detail endpoint returns the
full summary.
* Five new tests cover: empty-runs case, post-complete surface,
newest-of-multiple selection, empty-string skip, batch with
missing tasks + empty input.
Smoke-tested locally against the live profile DB on the three
acceptance-criterion targets (t_f08fef91 cron-hygiene-audit,
t_007b7f1c EMA-analysis, t_05746fa4 self-assessment) — all three now
return their populated summaries via both ``latest_summary`` and
``latest_summaries``.
Test plan: 255/255 kanban tests pass + 91/91 dashboard plugin tests
pass. No regression on tasks where ``tasks.result`` is explicitly
populated (the existing "Result:" branch is preserved).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes kanban task run handoffs visible to operators by surfacing the latest non-empty task_runs.summary as latest_summary in both the dashboard API and the CLI kanban show output, avoiding blank “Result” sections when workers complete via kanban_complete(summary=...).
Changes:
- Add
kanban_db.latest_summary()and batchkanban_db.latest_summaries()helpers to fetch the latest non-empty run summary (batch via a single window-function query). - Extend dashboard
/board(preview-truncated) and/tasks/{id}(full text) responses to includelatest_summaryper task. - Update
hermes kanban show <id>to print aLatest summary:block (and includelatest_summaryin JSON output) whentasks.resultis empty.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
hermes_cli/kanban_db.py |
Adds single-task and batch DB helpers to retrieve latest non-empty run summaries. |
plugins/kanban/dashboard/plugin_api.py |
Attaches latest_summary to task payloads for board cards (preview) and task detail (full). |
hermes_cli/kanban.py |
Enhances kanban show to surface the run summary when task.result is empty; JSON includes latest_summary. |
tests/hermes_cli/test_kanban_db.py |
Adds unit tests covering latest_summary/latest_summaries behavior and edge cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # row to exist with a later ended_at. | ||
| conn.execute( | ||
| "UPDATE tasks SET status='ready', completed_at=NULL WHERE id=?", | ||
| (t,), | ||
| ) | ||
| # Sleep 1s so the second run's ended_at is provably later than | ||
| # the first (complete_task uses int(time.time())). | ||
| time.sleep(1.05) |
| Picks the most recent run by ``ended_at`` (falling back to ``id`` | ||
| for ties or unfinished rows). Returns None if no run has a summary. | ||
| """ | ||
| row = conn.execute( | ||
| "SELECT summary FROM task_runs " | ||
| "WHERE task_id = ? AND summary IS NOT NULL AND summary != '' " | ||
| "ORDER BY COALESCE(ended_at, started_at) DESC, id DESC LIMIT 1", | ||
| (task_id,), |
| placeholders = ",".join("?" for _ in ids) | ||
| rows = conn.execute( | ||
| f""" | ||
| SELECT task_id, summary FROM ( | ||
| SELECT task_id, summary, | ||
| ROW_NUMBER() OVER ( | ||
| PARTITION BY task_id | ||
| ORDER BY COALESCE(ended_at, started_at) DESC, id DESC | ||
| ) AS rn | ||
| FROM task_runs | ||
| WHERE task_id IN ({placeholders}) | ||
| AND summary IS NOT NULL AND summary != '' | ||
| ) WHERE rn = 1 | ||
| """, | ||
| ids, | ||
| ).fetchall() | ||
| return {r["task_id"]: r["summary"] for r in rows} |
|
Merged via PR #20448 (rebase-merge, commit |
Summary
The kanban-worker skill (built into the gateway dispatcher's spawn prompt) instructs every worker to hand off via
kanban_complete(summary=..., metadata=...). That writes the summary onto the closingtask_runsrow, not ontotasks.result— the latter is left NULL unless the caller explicitly passesresult=.A glance at the dashboard or
hermes kanban show <id>then shows a blank "Result:" section even when the worker did real work. On a multi-lane host this caused a 2026-05-05 false-alarm ("Hermes did nothing") on a task whose run carried a 10-line completion summary.This PR surfaces the latest non-null run summary as
latest_summaryso the worker's actual handoff lands in front of operators.Changes
kanban_db.latest_summary(conn, task_id)andkanban_db.latest_summaries(conn, task_ids). The batch variant uses a single window-function SELECT so the dashboard board endpoint doesn't pay N+1 on multi-hundred-task boards.hermes kanban show <id>prints aLatest summary:block whentasks.resultis empty but a run has produced a summary. The existingResult:section still wins when populated, so any hand-edited results path is untouched. JSON output gains a top-levellatest_summaryfield./boardand/tasks/{id}now include alatest_summaryfield on every task. Cards on/boardcarry a 200-character preview; the drawer/detail endpoint returns the full summary.Test plan
latest_summary*unit tests pass.tests/hermes_cli/test_kanban_*+ 91/91 intests/plugins/test_kanban_dashboard_plugin.py+tests/tools/test_kanban_tools.py.t_f08fef91,t_007b7f1c,t_05746fa4) — all return populated summaries via both helpers.latest_summarypreview.hermes kanban show <id>on a task whose worker calledkanban_complete(summary=...)(e.g. one of the three above) and verify the newLatest summary:block.Back-compat
tasks.resultIS populated (manual completion viahermes kanban complete <id> --result \"...\") keep the existingResult:section —latest_summaryis only used as a fallback whentasks.resultis empty._task_dictaddslatest_summaryas a new keyword argument with a None default, so existing callers (create-task / update-task responses) work unchanged and emitlatest_summary: nullin their payloads.🤖 Generated with Claude Code