Skip to content

✨ feat(kanban): add move_task helper + hermes kanban move CLI - #12

Closed
cwest wants to merge 1 commit into
cwest/integrationfrom
topic/kanban-move-cli
Closed

✨ feat(kanban): add move_task helper + hermes kanban move CLI#12
cwest wants to merge 1 commit into
cwest/integrationfrom
topic/kanban-move-cli

Conversation

@cwest

@cwest cwest commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What

Slice 3 Build B of eliminating the multi-card review chain (parent card t_8fb5741d; slices 1-2 shipped as homestead PRs #56/#58). The one-card review model moves a single card through lanes (todo -> doing -> review -> merge -> done, plus the inner/outer review loops) instead of spawning a separate card per stage.

This PR gives that status transition a sanctioned home in kanban_db.py and an outer-loop CLI affordance:

  • move_task(conn, task_id, *, status, assignee=None, reason=None) — validates status against VALID_STATUSES, refuses to move a running (claimed) card (RuntimeError, same safety as assign_task), optionally reassigns (resetting the per-profile failure streak on an owner change, like assign_task), and emits a status_changed event. Idempotent: moving a card to the lane+owner it already occupies returns True with no spurious events, so a re-fired webhook or a repeated CLI invocation converges.
  • hermes kanban move <id> --lane <status> [--assignee <p>] [--reason <r>] — Casey's outer loop in one command: hermes kanban move t_abc --lane ready --assignee eckert returns a card from the merge lane to doing+eckert. --assignee none unassigns; omitting it leaves ownership untouched.

Why this shape

Before this, the three homestead webhook skills did the lane move inline with an UPDATE tasks SET status=? inside write_txn plus hand-emitted status_changed/assigned events, because kanban_db had no general status-move helper (assign_task = assignee only; complete_task/archive_task = one terminal status each). move_task formalises that exact pattern so webhook moves and CLI moves share one audited transition path.

Per AGENTS.md narrow-waist rule, this is a library helper + CLI command, NOT a new model tool — the core tool schema is unchanged.

Tests (TDD, RED -> GREEN)

  • move_task: status-only move + status_changed; status+reassign emits both events; reassign-without-status-change emits only assigned; reason recorded; status validation (ValueError); unknown task -> False; refuses running card; idempotent (no duplicate events); resets failure streak on reassign; assignee=None leaves ownership untouched.
  • CLI move: changes lane+assignee; lane-only; unknown id -> error+exit 1; bad lane -> ValueError surfaced, card unmoved.

Verification

  • Relevant suite (test_kanban_db.py + test_kanban_cli.py + test_kanban_core_functionality.py + tools/test_kanban_tools.py + test_kanban_decompose.py): 561 passed, 1 skipped, 0 failed.
  • Pre-existing red NOT introduced here: test_signal_handler_kanban_worker.py::test_sigterm_with_kanban_task_env_terminates_quickly fails identically on pristine origin/cwest/integration (verified against a clean baseline worktree). The test_kanban_decompose.py failures seen only in the full 700-test parallel run are cross-test ordering pollution — those tests pass in isolation and in the combined relevant suite above; not caused by this change.

Base branch

Targets cwest/integration (not main): the active kanban work — including the review-card dedup fixes slices 1-2 depend on — lives there (211+ lines ahead of main in kanban_db.py). Building on main would land move_task on a stale kanban_db.

Follow-up (out of scope)

  • Refactor the 3 homestead webhook skills' inline lane-move UPDATEs to call move_task (cross-repo; homestead).
  • Build A (close-pr-card MOVE->done) ships as a separate hermes-homestead PR.

Plan: docs/plans/2026-06-27-kanban-move-cli.md

The one-card review model moves a single card through lanes
(todo -> doing -> review -> merge -> done) instead of spawning a card per
stage. The homestead webhook skills (slices 1-2) perform that status
transition inline with an UPDATE inside write_txn plus hand-emitted
status_changed/assigned events, but kanban_db had no sanctioned home for
it: assign_task changes assignee only, complete_task/archive_task hit one
terminal status each.

Add move_task(conn, task_id, *, status, assignee=None, reason=None): it
validates status against VALID_STATUSES, refuses to move a running
(claimed) card, optionally reassigns (resetting the failure streak like
assign_task does), and emits a status_changed event — idempotent, so a
re-fired webhook or repeated CLI converges. Wire a `hermes kanban move
<id> --lane <status> [--assignee <p>] [--reason <r>]` subcommand on top of
it. This is Casey's outer-loop affordance: one command returns a card from
the merge lane to doing+eckert.

Per AGENTS.md narrow-waist rule this is a library helper + CLI command, NOT
a new model tool — the core tool schema is unchanged. The 3 webhook skills'
inline UPDATEs can later be refactored to call move_task (cross-repo
follow-up).

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff and ran it. The move_task helper does what the description says: it validates status against VALID_STATUSES, refuses to move a claimed running card, resets the failure streak on an owner change, and stays idempotent when the target lane and owner already match. SQL is parameterized and there's nothing security-relevant in the added lines.

I ran the suites the description cites and exercised the real CLI, not just the test harness:

  • tests/hermes_cli/test_kanban_db.py + test_kanban_cli.py: 283 passed, 0 failed. The 14 new tests (10 for move_task, 4 for the CLI) all pass.
  • hermes kanban move is wired into the real parser and dispatch (shows in --help with proper subcommand help). End-to-end against an isolated DB: a lane+owner move emits both status_changed and assigned, a lane-only move leaves ownership intact, --assignee none unassigns, and a repeated identical move adds no new events. Confirmed each against the actual task and task_events rows, not just stdout.

Two things worth a look, neither blocking. Holding this at a comment since it's still a draft.

Comment thread hermes_cli/kanban.py
print(f"no such task: {args.task_id}", file=sys.stderr)
return 1
if unassign:
kb.assign_task(conn, args.task_id, None)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unassign case runs as two separate transactions: move_task commits the lane change, then assign_task commits the unassign. If the process dies between them the card is moved but still owned. The lane move and the unassign aren't atomic the way a lane+reassign in a single move_task call is. Probably fine for the CLI's interactive use, but if a webhook ever drives an unassigning move it's a torn-write window. One option is to let move_task accept an explicit unassign sentinel so the whole thing lands in one write_txn.

Comment thread hermes_cli/kanban.py
)
if not ok:
print(f"no such task: {args.task_id}", file=sys.stderr)
return 1

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns 1, and kanban_command propagates it, but the process still exits 0 — I confirmed hermes kanban move t_nope --lane review and a bad --lane both print the error and exit 0. It's not introduced here: assign, complete, and show on an unknown id all exit 0 the same way, so the exit code is being dropped upstream in main(), outside this PR's surface. Flagging because the plan lists exit 1 / non-zero as acceptance for the unknown-id and bad-lane cases, and the two CLI tests assert on the printed string rather than the exit code, so they pass while the actual exit status doesn't meet the stated bar. A follow-up to fix the propagation in main() (and tighten those two tests to check the code) would close the gap.

@cwest

cwest commented Jun 29, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded-by-design. The one-card lifecycle moves cards between lanes via the sanctioned move_card helper (homestead skill machinery running inside write_txn with status_changed/assigned events), which covers the lane-transition need this CLI verb was for. A first-class 'move' CLI verb overlaps that existing path; not adding a second mechanism. The lane-motion verbs that remain (block/unblock/promote/complete/archive) plus move_card are sufficient.

@cwest cwest closed this Jun 29, 2026
@cwest
cwest deleted the topic/kanban-move-cli branch June 29, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant