Skip to content

feat(kanban): add awaiting_human_ops state for R3 gate cards - #27890

Closed
shunsuke-hikiyama wants to merge 2 commits into
NousResearch:mainfrom
shunsuke-hikiyama:feat/awaiting-human-ops-state
Closed

feat(kanban): add awaiting_human_ops state for R3 gate cards#27890
shunsuke-hikiyama wants to merge 2 commits into
NousResearch:mainfrom
shunsuke-hikiyama:feat/awaiting-human-ops-state

Conversation

@shunsuke-hikiyama

Copy link
Copy Markdown
Contributor

feat(kanban): add awaiting_human_ops state for R3 gate cards

Why

Today every "I'm waiting for a human" card lands in blocked, regardless of
whether 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 on
    purpose; operator decides go / no-go.
  • blocked = legacy meaning, now reserved for technical or dependency
    waits 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_change hooks unchanged, so
plugins keep working.

What

Schema

  • Extends VALID_STATUSES and VALID_INITIAL_STATUSES with
    awaiting_human_ops.
  • Adds a CHECK constraint on tasks.status covering the full enum.
  • Bumps _config_version to 24 and writes PRAGMA user_version = 24
    at the end of the migration.

SQLite cannot add a CHECK constraint in place, so the migration
(_migrate_tasks_status_check) rebuilds the tasks table:

  1. PRAGMA foreign_keys=OFF for the duration of the rebuild (restored on
    exit).
  2. ALTER TABLE tasks RENAME TO tasks__pre_v24_status_check.
  3. CREATE TABLE tasks with the new schema. The schema is built
    dynamically: every v24 column plus any legacy column found on the
    live table (e.g. spawn_failures from 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_columns
    continues to hold.
  4. INSERT INTO tasks SELECT ... over the column set common to both
    tables. Statuses are copied verbatim — no row is reclassified.
  5. DROP the temp table and rebuild every index the rebuild dropped via
    the new _ensure_indexes helper (which itself skips indexes whose
    underlying table doesn't exist yet, so legacy-DB tests with only
    tasks + task_events still pass).

The migration is idempotent: _tasks_status_check_needs_migration returns
False once the new CHECK is in place, and re-running init_db after that
is 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 takes target_status='blocked'|'awaiting_human_ops'
    (default 'blocked'). Validates the choice and routes the event payload
    accordingly.
  • unblock_task(...) accepts either waiting state and routes back to
    ready or todo based on parent completion, identical to the existing
    blocked flow.
  • complete_task(...) and reclaim_task(...) updated to recognize the
    new state in their WHERE clauses (so a card can be completed or
    reclaimed straight from awaiting_human_ops).
  • New move_task(conn, task_id, new_status) helper backs both the CLI
    move subcommand and the dashboard drag/drop. Refuses direct moves to
    running (must go through claim/dispatch), routes done / archived
    through the existing helpers, treats awaiting_human_ops <-> blocked
    as a permitted manual move (CHECK constraint still enforced).

CLI

  • hermes kanban block --status awaiting_human_ops flag.
  • hermes kanban move <task_id> <status> subcommand (choices come from
    VALID_STATUSES).
  • hermes kanban list --status awaiting_human_ops and
    stats formatting both widened to fit the longer status name.
  • New ! glyph for awaiting_human_ops (distinct from for blocked).

Tools (LLM-facing)

  • kanban_block schema gains a status enum; default unchanged.
  • kanban_create.initial_status enum extended to include the new state.
  • Result payloads echo the chosen waiting state so the model can confirm.

Dashboard

  • New "Awaiting Human Ops" column between running and blocked in the
    bundled dist/index.js (COLUMN_ORDER and English fallback dicts).
  • Purple .hermes-kanban-dot-awaiting-human-ops swatch (vs the
    destructive red used for blocked).
  • New "Await ops" bulk action button + per-card Await ops button,
    guarded by the existing destructive-confirm flow.
  • Staleness thresholds for the new state (amber=4h, red=48h) — more
    generous than blocked because human ops review can take a working day.
  • plugin_api.update_task / bulk_update route awaiting_human_ops
    through block_task(target_status=...) (from running/ready) or
    move_task (from another waiting state), unblock recognizes either
    waiting state.

i18n

  • en.ts / ja.ts: full translations for the status label, dashboard
    description, and destructive-confirm copy. Japanese label is
    「ops承認待ち」per the spec.
  • All 14 remaining locale files (af, de, es, fr, ga, hu,
    it, ko, pt, ru, tr, uk, zh, zh-hant) get the same
    three keys with English fallback strings, matching the existing
    fallback pattern from PR fix: align threading docstring with implementation #27 / PR-4.
  • types.ts updated; this is a non-optional Translations extension,
    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:

ID Test What it proves
T1 test_t1_new_card_default_status_unchanged The new state is opt-in; a plain kanban add still lands in ready.
T2 test_t2_initial_status_awaiting_human_ops Cards can be parked directly in awaiting_human_ops; bad enum values rejected.
T3 test_t3_unblock_from_awaiting_human_ops unblock reopens a card from the new state, matching the existing blocked contract.
T4 test_t4_cli_move_awaiting_human_ops kanban move <id> awaiting_human_ops works; argparse rejects bogus statuses.
T5 test_t5_filter_state_awaiting_human_ops list --status awaiting_human_ops returns only matching cards.
T6 test_t6_migration_keeps_existing_blocked_cards A pre-existing blocked card is not reclassified after the v24 migration; the migration is idempotent (running init_db twice is a no-op); creating a new awaiting_human_ops card 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 skipped
  • PYTHONUTF8=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 on origin/main without this PR's changes — it's a pre-existing baseline flake on Windows, not a regression.

Backward compatibility

  • No existing blocked row is reclassified. Operators who want to
    re-bucket historical R3-gate cards into awaiting_human_ops do so
    manually via kanban move <id> awaiting_human_ops or the dashboard
    bulk action. The PR explicitly avoids any heuristic auto-migration
    because the historical mixed semantics make it impossible to do
    safely without operator review.
  • Existing pre_state_change / post_state_change hooks fire for the
    new state too — no new hook surface is added.
  • The CLI / tools APIs that already accept 'blocked' continue to work
    identically; awaiting_human_ops is purely additive.

Migration notes (forward-only)

The schema migration is forward-only. Once a DB has run on v24:

  1. Any new awaiting_human_ops rows are persisted.
  2. Downgrading to v23 (or any binary without the new value in the CHECK
    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):

  1. Stop all dispatchers / workers writing to the kanban DB.
  2. Move every awaiting_human_ops card back to blocked:
    UPDATE tasks SET status = 'blocked'
     WHERE status = 'awaiting_human_ops';
  3. Drop the CHECK constraint by rebuilding the table with the old
    schema (mirror of _migrate_tasks_status_check but with the v23
    column list).
  4. Reset PRAGMA user_version = 23 and _config_version = 23 in
    ~/.hermes/config.yaml.
  5. Restart on the v23 binary.

This is intentionally not automated: rollbacks should be rare and
operator-supervised. The PR keeps the migration idempotent so accidental
re-runs of init_db on a v24 DB are safe.

Files changed

  • hermes_cli/kanban_db.py: enum, CHECK constraint, migration helpers,
    state-machine updates, move_task helper.
  • hermes_cli/kanban.py: move subcommand, block --status flag,
    list/stats formatting widths, status icon.
  • hermes_cli/config.py: _config_version bump.
  • tools/kanban_tools.py: tool schemas for kanban_block and
    kanban_create, handler routing.
  • plugins/kanban/dashboard/plugin_api.py: board column order,
    update/bulk routing.
  • plugins/kanban/dashboard/dist/index.js / dist/style.css: rendered
    dashboard 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.

QuoQuo added 2 commits May 18, 2026 17:31
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>
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets javascript labels May 18, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks @shunsuke-hikiyama — closing this one. The awaiting_human_ops status addition (with full SQLite table-rebuild migration to user_version 24 plus 14 locale files) is a substantive lifecycle change. Main now has initial_status='blocked' (your other PR #27526 salvaged) which covers the human-ops case via the existing blocked status. If awaiting_human_ops adds something blocked doesn't (different notifier behavior? different dashboard column?), it would help to lay that out in an issue. Appreciate the work.

@teknium1 teknium1 closed this May 19, 2026
@shunsuke-hikiyama

Copy link
Copy Markdown
Contributor Author

Fair point — blocked covers the human-ops case in practice with initial_status='blocked' now merged.

If awaiting_human_ops semantics ever become useful (separate notifier behavior, distinct dashboard column for "this needs human R3 sign-off" vs "stuck on dependency"), I'll file an issue first to lay out the case before any code. Thanks for the consideration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants