mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-12 03:42:08 +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
|
|
@ -1071,10 +1071,16 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|||
parents = kb.parent_ids(conn, args.task_id)
|
||||
children = kb.child_ids(conn, args.task_id)
|
||||
runs = kb.list_runs(conn, args.task_id)
|
||||
# Workers hand off via ``task_runs.summary`` (kanban-worker skill);
|
||||
# ``tasks.result`` is left NULL unless the caller explicitly passed
|
||||
# ``result=``. Surfacing the latest summary here keeps ``show`` from
|
||||
# looking like a no-op when the worker actually did real work.
|
||||
latest_summary = kb.latest_summary(conn, args.task_id)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
payload = {
|
||||
"task": _task_to_dict(task),
|
||||
"latest_summary": latest_summary,
|
||||
"parents": parents,
|
||||
"children": children,
|
||||
"comments": [
|
||||
|
|
@ -1161,6 +1167,13 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|||
print()
|
||||
print("Result:")
|
||||
print(task.result)
|
||||
elif latest_summary:
|
||||
# Worker handoff lives on the latest run, not on tasks.result.
|
||||
# Surface it at top-level so a glance at ``hermes kanban show <id>``
|
||||
# tells you what the worker did even if tasks.result is empty.
|
||||
print()
|
||||
print("Latest summary:")
|
||||
print(latest_summary)
|
||||
if comments:
|
||||
print()
|
||||
print(f"Comments ({len(comments)}):")
|
||||
|
|
|
|||
|
|
@ -4013,3 +4013,61 @@ def latest_run(conn: sqlite3.Connection, task_id: str) -> Optional[Run]:
|
|||
(task_id,),
|
||||
).fetchone()
|
||||
return Run.from_row(row) if row else None
|
||||
|
||||
|
||||
def latest_summary(conn: sqlite3.Connection, task_id: str) -> Optional[str]:
|
||||
"""Return the latest non-null ``task_runs.summary`` for ``task_id``.
|
||||
|
||||
The kanban-worker skill writes its handoff to ``task_runs.summary``
|
||||
via ``complete_task(summary=...)``; ``tasks.result`` is left empty
|
||||
unless the caller passes ``result=`` explicitly. Dashboards and CLI
|
||||
"show" views need this value to surface what a worker actually did
|
||||
— without it, ``tasks.result`` is NULL and the task looks like a
|
||||
no-op even when the run completed.
|
||||
|
||||
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,),
|
||||
).fetchone()
|
||||
return row["summary"] if row else None
|
||||
|
||||
|
||||
def latest_summaries(
|
||||
conn: sqlite3.Connection, task_ids: Iterable[str]
|
||||
) -> dict[str, str]:
|
||||
"""Batch-fetch latest non-null summaries for a list of task ids.
|
||||
|
||||
Used by the dashboard board endpoint to attach ``latest_summary`` to
|
||||
every card in a single SQL query, avoiding the N+1 pattern of
|
||||
calling :func:`latest_summary` per task. Returns a dict mapping
|
||||
``task_id`` → summary string, omitting tasks with no summary.
|
||||
|
||||
Approach: a window function picks the newest non-null-summary row
|
||||
per ``task_id``; works against SQLite ≥ 3.25 (default on every
|
||||
supported platform).
|
||||
"""
|
||||
ids = list(task_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue