mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
feat(kanban): surface task_runs.summary on dashboard cards + `kanban 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>
This commit is contained in:
parent
d2c6eceed9
commit
3f97297413
4 changed files with 177 additions and 3 deletions
|
|
@ -124,11 +124,23 @@ BOARD_COLUMNS: list[str] = [
|
|||
]
|
||||
|
||||
|
||||
def _task_dict(task: kanban_db.Task) -> dict[str, Any]:
|
||||
_CARD_SUMMARY_PREVIEW_CHARS = 200
|
||||
|
||||
|
||||
def _task_dict(
|
||||
task: kanban_db.Task,
|
||||
*,
|
||||
latest_summary: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
d = asdict(task)
|
||||
# Add derived age metrics so the UI can colour stale cards without
|
||||
# computing deltas client-side.
|
||||
d["age"] = kanban_db.task_age(task)
|
||||
# Surface the latest non-null run summary so dashboards don't show
|
||||
# blank cards/drawers for tasks where the worker handed off via
|
||||
# ``task_runs.summary`` (the kanban-worker pattern) instead of
|
||||
# ``tasks.result``. ``None`` when no run has produced a summary yet.
|
||||
d["latest_summary"] = latest_summary
|
||||
# Keep body short on list endpoints; full body comes from /tasks/:id.
|
||||
return d
|
||||
|
||||
|
|
@ -381,8 +393,18 @@ def get_board(
|
|||
if include_archived:
|
||||
columns["archived"] = []
|
||||
|
||||
# Batch-fetch the latest non-null run summary per task in one
|
||||
# window-function query (avoids N+1 ``latest_summary`` calls
|
||||
# for boards with hundreds of tasks). Truncated to a card-size
|
||||
# preview here — the full text is available via /tasks/:id.
|
||||
summary_map = kanban_db.latest_summaries(conn, [t.id for t in tasks])
|
||||
|
||||
for t in tasks:
|
||||
d = _task_dict(t)
|
||||
full = summary_map.get(t.id)
|
||||
preview = (
|
||||
full[:_CARD_SUMMARY_PREVIEW_CHARS] if full else None
|
||||
)
|
||||
d = _task_dict(t, latest_summary=preview)
|
||||
d["link_counts"] = link_counts.get(t.id, {"parents": 0, "children": 0})
|
||||
d["comment_count"] = comment_counts.get(t.id, 0)
|
||||
d["progress"] = progress.get(t.id) # None when the task has no children
|
||||
|
|
@ -440,7 +462,11 @@ def get_task(task_id: str, board: Optional[str] = Query(None)):
|
|||
task = kanban_db.get_task(conn, task_id)
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail=f"task {task_id} not found")
|
||||
task_d = _task_dict(task)
|
||||
# Drawer/detail view returns the FULL summary (no truncation) so
|
||||
# operators can read the complete worker handoff without making
|
||||
# a second round-trip. Cards on /board carry a 200-char preview.
|
||||
full_summary = kanban_db.latest_summary(conn, task_id)
|
||||
task_d = _task_dict(task, latest_summary=full_summary)
|
||||
# Attach diagnostics so the drawer's Diagnostics section can
|
||||
# render recovery actions without a second round-trip.
|
||||
diags = _compute_task_diagnostics(conn, task_ids=[task_id])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue