feat(kanban): add awaiting_human_ops state for R3 gate cards - #27890
feat(kanban): add awaiting_human_ops state for R3 gate cards#27890shunsuke-hikiyama wants to merge 2 commits into
Conversation
Extends the kanban status enum from
{triage, todo, ready, running, blocked, done, archived}
to
{triage, todo, ready, running, blocked, awaiting_human_ops, done, archived}
so the R3 human-ops approval gate can be separated from technical /
dependency blockers in the CLI, dashboard and metrics.
* schema (v23 -> v24): add a CHECK constraint on tasks.status. SQLite
needs a table rebuild for this; the migration preserves all row
statuses verbatim (no auto-reclassify of existing 'blocked' cards),
preserves any legacy columns still present (e.g. spawn_failures),
rebuilds indexes via the new _ensure_indexes helper, and stamps
PRAGMA user_version = 24. Idempotent: re-running init_db is a no-op
once awaiting_human_ops is in the CHECK clause.
* state machine: block_task() accepts target_status='awaiting_human_ops',
unblock_task() returns either waiting state to ready/todo, complete /
reclaim accept the new state, and a new move_task() helper backs both
the CLI and dashboard manual moves.
* CLI: `hermes kanban block --status awaiting_human_ops` and the new
`hermes kanban move <id> <status>` subcommand; list/filter already
use VALID_STATUSES so --status awaiting_human_ops works automatically.
* tools: kanban_block accepts status='awaiting_human_ops'; kanban_create
initial_status enum extended.
* dashboard: new "Awaiting Human Ops" column between running and
blocked, distinct purple dot, staleness thresholds, bulk-action
button, per-card Await ops button, destructive confirm copy.
* i18n: en/ja localized for status label, description and destructive
confirm; the other 14 locales get the new keys with English fallback
strings (matching the existing fallback pattern).
Backward compatibility: no existing 'blocked' card is reclassified;
the new state is strictly opt-in. The migration is forward-only —
downgrading to v23 will reject any 'awaiting_human_ops' rows at the
CHECK constraint; operators must move them back to 'blocked' first.
Tests:
- tests/hermes_cli/test_kanban_cli.py: T1-T6 cover default-state opt-in
(T1), initial_status awaiting_human_ops (T2), unblock from the new
state (T3), `kanban move` to and rejection of bad status (T4),
`list --status awaiting_human_ops` filter (T5), idempotent v23->v24
migration that leaves existing 'blocked' rows untouched (T6).
- All 443 hermes_cli kanban tests pass (PYTHONUTF8=1, --extra dev
--extra web).
Signed-off-by: QuoQuo <quoquoquants@gmail.com>
|
Thanks @shunsuke-hikiyama — closing this one. The |
|
Fair point — If |
feat(kanban): add
awaiting_human_opsstate for R3 gate cardsWhy
Today every "I'm waiting for a human" card lands in
blocked, regardless ofwhether the worker hit a technical/dependency wait, asked an open-ended
question, or paused on an R3 human-ops approval gate (ADR-013). The
dashboard, CLI filters, and any external metric collector cannot tell those
apart — operators see one undifferentiated red column.
This PR splits the human-ops case out so:
awaiting_human_ops= R3 approval gate: orchestrator parked the card onpurpose; operator decides go / no-go.
blocked= legacy meaning, now reserved for technical or dependencywaits the worker can't resolve itself.
The two are distinguishable in every UI layer (CLI list, CLI filter,
dashboard column, dashboard color, bulk action) and pass through the
existing
pre_state_change/post_state_changehooks unchanged, soplugins keep working.
What
Schema
VALID_STATUSESandVALID_INITIAL_STATUSESwithawaiting_human_ops.CHECKconstraint ontasks.statuscovering the full enum._config_versionto 24 and writesPRAGMA user_version = 24at the end of the migration.
SQLite cannot add a
CHECKconstraint in place, so the migration(
_migrate_tasks_status_check) rebuilds thetaskstable:PRAGMA foreign_keys=OFFfor the duration of the rebuild (restored onexit).
ALTER TABLE tasks RENAME TO tasks__pre_v24_status_check.CREATE TABLE taskswith the new schema. The schema is builtdynamically: every v24 column plus any legacy column found on the
live table (e.g.
spawn_failuresfrom pre-Kanban migration fails after update: no such column spawn_failures #20842 DBs) is included,so the ADD-first-then-copy contract from
_migrate_add_optional_columnscontinues to hold.
INSERT INTO tasks SELECT ...over the column set common to bothtables. Statuses are copied verbatim — no row is reclassified.
DROPthe temp table and rebuild every index the rebuild dropped viathe new
_ensure_indexeshelper (which itself skips indexes whoseunderlying table doesn't exist yet, so legacy-DB tests with only
tasks+task_eventsstill pass).The migration is idempotent:
_tasks_status_check_needs_migrationreturnsFalse once the new CHECK is in place, and re-running
init_dbafter thatis a no-op.
A guard rejects the migration if any row has a status outside the v24 enum
— prevents data loss from a DB that someone hand-edited.
State machine
block_task(...)now takestarget_status='blocked'|'awaiting_human_ops'(default
'blocked'). Validates the choice and routes the event payloadaccordingly.
unblock_task(...)accepts either waiting state and routes back toreadyortodobased on parent completion, identical to the existingblockedflow.complete_task(...)andreclaim_task(...)updated to recognize thenew state in their WHERE clauses (so a card can be completed or
reclaimed straight from
awaiting_human_ops).move_task(conn, task_id, new_status)helper backs both the CLImovesubcommand and the dashboard drag/drop. Refuses direct moves torunning(must go through claim/dispatch), routesdone/archivedthrough the existing helpers, treats
awaiting_human_ops <-> blockedas a permitted manual move (CHECK constraint still enforced).
CLI
hermes kanban block --status awaiting_human_opsflag.hermes kanban move <task_id> <status>subcommand (choices come fromVALID_STATUSES).hermes kanban list --status awaiting_human_opsandstatsformatting both widened to fit the longer status name.!glyph for awaiting_human_ops (distinct from⊘for blocked).Tools (LLM-facing)
kanban_blockschema gains astatusenum; default unchanged.kanban_create.initial_statusenum extended to include the new state.Dashboard
runningandblockedin thebundled
dist/index.js(COLUMN_ORDERand English fallback dicts)..hermes-kanban-dot-awaiting-human-opsswatch (vs thedestructive red used for
blocked).Await opsbutton,guarded by the existing destructive-confirm flow.
amber=4h,red=48h) — moregenerous than
blockedbecause human ops review can take a working day.plugin_api.update_task/bulk_updaterouteawaiting_human_opsthrough
block_task(target_status=...)(from running/ready) ormove_task(from another waiting state), unblock recognizes eitherwaiting state.
i18n
en.ts/ja.ts: full translations for the status label, dashboarddescription, and destructive-confirm copy. Japanese label is
「ops承認待ち」per the spec.
af,de,es,fr,ga,hu,it,ko,pt,ru,tr,uk,zh,zh-hant) get the samethree keys with English fallback strings, matching the existing
fallback pattern from PR fix: align threading docstring with implementation #27 / PR-4.
types.tsupdated; this is a non-optionalTranslationsextension,so any locale missing the keys would fail typecheck — the bulk
fallback above keeps the project building.
Tests
New tests in
tests/hermes_cli/test_kanban_cli.py:test_t1_new_card_default_status_unchangedkanban addstill lands inready.test_t2_initial_status_awaiting_human_opsawaiting_human_ops; bad enum values rejected.test_t3_unblock_from_awaiting_human_opsunblockreopens a card from the new state, matching the existingblockedcontract.test_t4_cli_move_awaiting_human_opskanban move <id> awaiting_human_opsworks; argparse rejects bogus statuses.test_t5_filter_state_awaiting_human_opslist --status awaiting_human_opsreturns only matching cards.test_t6_migration_keeps_existing_blocked_cardsblockedcard is not reclassified after the v24 migration; the migration is idempotent (runninginit_dbtwice is a no-op); creating a newawaiting_human_opscard after migration works.Results (Windows native, Python 3.12.10):
PYTHONUTF8=1 uv run --extra dev --extra web pytest tests/hermes_cli -k kanban -q->443 passed, 4 skippedPYTHONUTF8=1 uv run --extra dev --extra web pytest tests/plugins/test_kanban_dashboard_plugin.py -q->80 passed, 1 failed. The failing test (test_diagnostics_endpoint_severity_filter) fails identically onorigin/mainwithout this PR's changes — it's a pre-existing baseline flake on Windows, not a regression.Backward compatibility
blockedrow is reclassified. Operators who want tore-bucket historical R3-gate cards into
awaiting_human_opsdo somanually via
kanban move <id> awaiting_human_opsor the dashboardbulk action. The PR explicitly avoids any heuristic auto-migration
because the historical mixed semantics make it impossible to do
safely without operator review.
pre_state_change/post_state_changehooks fire for thenew state too — no new hook surface is added.
'blocked'continue to workidentically;
awaiting_human_opsis purely additive.Migration notes (forward-only)
The schema migration is forward-only. Once a DB has run on v24:
awaiting_human_opsrows are persisted.clause) would either fail to open the DB (if it re-runs migrations
and finds an unknown CHECK term) or accept the rows but reject any
subsequent write to them.
Rollback procedure (for operators who need to revert):
awaiting_human_opscard back toblocked:schema (mirror of
_migrate_tasks_status_checkbut with the v23column list).
PRAGMA user_version = 23and_config_version = 23in~/.hermes/config.yaml.This is intentionally not automated: rollbacks should be rare and
operator-supervised. The PR keeps the migration idempotent so accidental
re-runs of
init_dbon a v24 DB are safe.Files changed
hermes_cli/kanban_db.py: enum, CHECK constraint, migration helpers,state-machine updates,
move_taskhelper.hermes_cli/kanban.py:movesubcommand,block --statusflag,list/stats formatting widths, status icon.
hermes_cli/config.py:_config_versionbump.tools/kanban_tools.py: tool schemas forkanban_blockandkanban_create, handler routing.plugins/kanban/dashboard/plugin_api.py: board column order,update/bulk routing.
plugins/kanban/dashboard/dist/index.js/dist/style.css: rendereddashboard bundle (column, color, action buttons, staleness, confirm copy).
web/src/i18n/types.ts+ all 16 locale files: new translation keys.tests/hermes_cli/test_kanban_cli.py: T1-T6.