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
|
|
@ -822,3 +822,80 @@ class TestSharedBoardPaths:
|
|||
default_home / "kanban" / "workspaces"
|
||||
)
|
||||
assert env["HERMES_KANBAN_TASK"] == "t_dispatch_env"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# latest_summary / latest_summaries — surface task_runs.summary handoffs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_latest_summary_returns_none_when_no_runs(kanban_home):
|
||||
"""A freshly-created task has no runs and therefore no summary."""
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="fresh", assignee="alice")
|
||||
assert kb.latest_summary(conn, t) is None
|
||||
|
||||
|
||||
def test_latest_summary_returns_summary_after_complete(kanban_home):
|
||||
"""``complete_task(summary=...)`` is the canonical kanban-worker
|
||||
handoff; ``latest_summary`` must surface it so dashboards/CLI can
|
||||
render what the worker actually did."""
|
||||
handoff = "shipped 3 files, ran tests, opened PR #42"
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="work", assignee="alice")
|
||||
kb.complete_task(conn, t, summary=handoff)
|
||||
assert kb.latest_summary(conn, t) == handoff
|
||||
|
||||
|
||||
def test_latest_summary_picks_newest_when_multiple_runs(kanban_home):
|
||||
"""When a task has been re-run (block → unblock → complete), the
|
||||
newest run's summary wins. We unblock to take the task back to
|
||||
``ready``, then complete a second time and verify the second
|
||||
summary surfaces."""
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="retry", assignee="alice")
|
||||
kb.complete_task(conn, t, summary="first attempt")
|
||||
# Move back to ready by direct SQL — block_task / unblock_task
|
||||
# paths require an active claim, but we just want a second run
|
||||
# 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)
|
||||
kb.complete_task(conn, t, summary="second attempt — final")
|
||||
assert kb.latest_summary(conn, t) == "second attempt — final"
|
||||
|
||||
|
||||
def test_latest_summary_skips_empty_string(kanban_home):
|
||||
"""A run with an empty-string summary should not mask an earlier
|
||||
populated one — empty strings carry no information."""
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="t", assignee="alice")
|
||||
kb.complete_task(conn, t, summary="real handoff")
|
||||
# Inject a later run with empty summary directly. Workers
|
||||
# writing "" instead of None is a real shape we want to ignore.
|
||||
conn.execute(
|
||||
"INSERT INTO task_runs (task_id, status, started_at, ended_at, "
|
||||
"outcome, summary) VALUES (?, 'done', ?, ?, 'completed', ?)",
|
||||
(t, int(time.time()) + 1, int(time.time()) + 2, ""),
|
||||
)
|
||||
conn.commit()
|
||||
assert kb.latest_summary(conn, t) == "real handoff"
|
||||
|
||||
|
||||
def test_latest_summaries_batch_omits_tasks_without_summary(kanban_home):
|
||||
"""``latest_summaries`` is the dashboard's N+1 escape hatch — it
|
||||
must return only entries for tasks that actually have a summary,
|
||||
keep the per-task latest, and accept an empty input gracefully."""
|
||||
with kb.connect() as conn:
|
||||
t1 = kb.create_task(conn, title="a", assignee="alice")
|
||||
t2 = kb.create_task(conn, title="b", assignee="bob")
|
||||
t3 = kb.create_task(conn, title="c", assignee="carol")
|
||||
kb.complete_task(conn, t1, summary="alpha")
|
||||
kb.complete_task(conn, t3, summary="charlie")
|
||||
out = kb.latest_summaries(conn, [t1, t2, t3])
|
||||
assert out == {t1: "alpha", t3: "charlie"}
|
||||
# Empty input → empty dict, no SQL syntax error from "IN ()".
|
||||
assert kb.latest_summaries(conn, []) == {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue