Operator UI Parity And Global Pause Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make WUI and TUI expose the same operator commands, labels, hotkeys, and backend behavior, with Pause/Resume controlling workers globally rather than freezing UI refresh.
Architecture: Add a DB-backed singleton control state and expose it through repository methods plus admin HTTP endpoints. Worker claim/execution paths read that state at safe boundaries. WUI and TUI both consume the same command contract: p pauses workers, R resumes workers, r refreshes the view, and review decisions use the same payload semantics.
Tech Stack: Python 3.12, asyncpg, Alembic, FastAPI, Pydantic v2, Jinja2/HTMX/SSE, Rich TUI, pytest/pytest-asyncio.
Files
- Create:
whilly/adapters/db/migrations/versions/014_control_state.py - Modify:
whilly/adapters/db/schema.sql - Modify:
whilly/adapters/db/repository.py - Modify:
whilly/adapters/transport/schemas.py - Modify:
whilly/adapters/transport/server.py - Modify:
whilly/adapters/transport/client.py - Modify:
whilly/operator_views.py - Modify:
whilly/api/dashboard.py - Modify:
whilly/api/templates/index.html.j2 - Modify:
whilly/cli/tui.py - Modify:
whilly/worker/local.py - Modify:
whilly/worker/remote.py - Test:
tests/unit/test_control_state_repository.py - Test:
tests/unit/test_tui.py - Test:
tests/unit/test_remote_worker.py - Test:
tests/unit/test_local_worker.py - Test:
tests/unit/test_transport_schemas.py - Test:
tests/integration/test_control_state_admin_api.py - Test:
tests/integration/test_htmx_dashboard.py - Test:
tests/integration/test_alembic_full_chain.py
Contract
| Command | WUI | TUI | Backend meaning |
|---|---|---|---|
p | Pause button and hotkey | Pause command | Set global worker pause. UI keeps refreshing. |
R | Resume button and hotkey | Resume command | Clear global worker pause. |
r | Refresh button/hotkey | Refresh command | Refresh the operator view only. |
q | Stop live page updates | Exit TUI | Leave the operator session. Does not pause workers. |
1-5 | Switch tabs | Switch surfaces | Same surface order. |
/ | Focus filter input | Enter filter mode | Filter rows on current view data. |
j/k | Select review gap on Compliance only | Select review gap on Compliance only | Move selected actionable review row. |
a/x/c | Approve/reject/request changes on Compliance only | Same | Record same decision payload shape. |
Task 1: Control-State Data Model
Files:
- Create:
whilly/adapters/db/migrations/versions/014_control_state.py - Modify:
whilly/adapters/db/schema.sql -
Test:
tests/integration/test_alembic_full_chain.py - Step 1: Add failing migration/full-chain expectation
Add assertions that the upgraded schema contains control_state with singleton-compatible columns:
rows = await conn.fetch(
"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'control_state'
ORDER BY ordinal_position
"""
)
assert [row["column_name"] for row in rows] == [
"id",
"paused",
"pause_reason",
"paused_by",
"paused_at",
"updated_at",
]
- Step 2: Run test to verify it fails
Run: .venv/bin/python -m pytest -q tests/integration/test_alembic_full_chain.py --maxfail=1
Expected: FAIL because control_state does not exist.
- Step 3: Add migration
Create revision 014_control_state after 013_work_intents_repo_targets:
op.create_table(
"control_state",
sa.Column("id", sa.Text(), primary_key=True),
sa.Column("paused", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("pause_reason", sa.Text(), nullable=True),
sa.Column("paused_by", sa.Text(), nullable=True),
sa.Column("paused_at", postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text("NOW()")),
sa.CheckConstraint("id = 'global'", name="ck_control_state_singleton"),
)
- Step 4: Update reference schema
Add the same table to whilly/adapters/db/schema.sql near other singleton/operator tables.
- Step 5: Run migration test
Run: .venv/bin/python -m pytest -q tests/integration/test_alembic_full_chain.py --maxfail=1
Expected: PASS.
- Step 6: Commit
Run:
git add whilly/adapters/db/migrations/versions/014_control_state.py whilly/adapters/db/schema.sql tests/integration/test_alembic_full_chain.py
git commit -m "feat(control): add global control state schema"
Task 2: Repository Control-State API
Files:
- Modify:
whilly/adapters/db/repository.py -
Test:
tests/unit/test_control_state_repository.py - Step 1: Write failing repository tests
Cover:
state = await repo.get_control_state()
assert state.paused is False
paused = await repo.pause_workers(reason="deploy", operator="lead@example.com")
assert paused.paused is True
assert paused.pause_reason == "deploy"
assert paused.paused_by == "lead@example.com"
resumed = await repo.resume_workers(operator="lead@example.com")
assert resumed.paused is False
assert await repo.is_workers_paused() is False
- Step 2: Run test to verify it fails
Run: .venv/bin/python -m pytest -q tests/unit/test_control_state_repository.py --maxfail=1
Expected: FAIL because methods/types do not exist.
- Step 3: Implement repository methods
Add a frozen ControlState dataclass and methods:
async def get_control_state(self) -> ControlState: ...
async def pause_workers(self, *, reason: str | None, operator: str | None) -> ControlState: ...
async def resume_workers(self, *, operator: str | None) -> ControlState: ...
async def is_workers_paused(self) -> bool: ...
get_control_state() must create the singleton row on first read using INSERT ... ON CONFLICT DO NOTHING.
- Step 4: Run repository tests
Run: .venv/bin/python -m pytest -q tests/unit/test_control_state_repository.py --maxfail=1
Expected: PASS.
- Step 5: Commit
Run:
git add whilly/adapters/db/repository.py tests/unit/test_control_state_repository.py
git commit -m "feat(control): add repository pause state"
Task 3: Admin API And Schemas
Files:
- Modify:
whilly/adapters/transport/schemas.py - Modify:
whilly/adapters/transport/server.py - Test:
tests/unit/test_transport_schemas.py -
Test:
tests/integration/test_control_state_admin_api.py - Step 1: Write failing schema/API tests
Cover:
pause = ControlPauseRequest(reason="deploy")
assert pause.reason == "deploy"
state = ControlStateResponse(paused=True, pause_reason="deploy", paused_by="lead@example.com")
assert state.paused is True
And integration:
await repo.mint_bootstrap_token("admin-token", owner_email="lead@example.com", is_admin=True)
response = await client.post("/api/v1/admin/workers/pause", json={"reason": "deploy"}, headers=admin_headers)
assert response.status_code == 200
assert response.json()["paused"] is True
response = await client.post("/api/v1/admin/workers/resume", headers=admin_headers)
assert response.status_code == 200
assert response.json()["paused"] is False
- Step 2: Run tests to verify failure
Run: .venv/bin/python -m pytest -q tests/unit/test_transport_schemas.py tests/integration/test_control_state_admin_api.py --maxfail=1
Expected: FAIL because schemas/routes do not exist.
- Step 3: Implement schemas and routes
Add:
class ControlPauseRequest(_FrozenModel):
reason: str = ""
class ControlStateResponse(_FrozenModel):
paused: bool
pause_reason: str | None = None
paused_by: str | None = None
paused_at: datetime | None = None
updated_at: datetime
Add admin routes:
GET /api/v1/admin/workers/control-state
POST /api/v1/admin/workers/pause
POST /api/v1/admin/workers/resume
Use request.state.bootstrap_owner_email as operator when available.
- Step 4: Run API tests
Run: .venv/bin/python -m pytest -q tests/unit/test_transport_schemas.py tests/integration/test_control_state_admin_api.py --maxfail=1
Expected: PASS.
- Step 5: Commit
Run:
git add whilly/adapters/transport/schemas.py whilly/adapters/transport/server.py tests/unit/test_transport_schemas.py tests/integration/test_control_state_admin_api.py
git commit -m "feat(control): expose admin pause API"
Task 4: Worker Enforcement
Files:
- Modify:
whilly/adapters/db/repository.py - Modify:
whilly/adapters/transport/client.py - Modify:
whilly/worker/local.py - Modify:
whilly/worker/remote.py - Test:
tests/unit/test_local_worker.py -
Test:
tests/unit/test_remote_worker.py - Step 1: Write failing worker tests
Local worker:
repo.pause_workers.return_value = paused_state
stats = await run_local_worker(repo, runner, plan, worker_id, idle_wait=0, max_iterations=1)
assert repo.claim_task.await_count == 0
assert stats.idle_polls == 1
Remote worker:
client.control_state_results.append(paused_state)
stats = await run_remote_worker(client, runner, plan, worker_id, max_iterations=1)
assert client.claim_calls == []
assert stats.idle_polls == 1
Active-task release:
repo.is_workers_paused.side_effect = [False, True]
assert release.reason == "operator_pause"
- Step 2: Run worker tests to verify failure
Run: .venv/bin/python -m pytest -q tests/unit/test_local_worker.py tests/unit/test_remote_worker.py --maxfail=1
Expected: FAIL because workers do not check control state.
- Step 3: Implement enforcement
Local worker checks repo.get_control_state() before claim and after runner/verification before complete/fail. If paused after claiming, release the task with operator_pause.
Remote worker adds RemoteWorkerClient.control_state() and checks before claim and after runner/verification before complete/fail. If paused after claiming, release with operator_pause.
- Step 4: Run worker tests
Run: .venv/bin/python -m pytest -q tests/unit/test_local_worker.py tests/unit/test_remote_worker.py --maxfail=1
Expected: PASS.
- Step 5: Commit
Run:
git add whilly/adapters/transport/client.py whilly/worker/local.py whilly/worker/remote.py tests/unit/test_local_worker.py tests/unit/test_remote_worker.py
git commit -m "feat(control): pause workers at safe checkpoints"
Task 5: Operator Snapshot And WUI Parity
Files:
- Modify:
whilly/operator_views.py - Modify:
whilly/api/dashboard.py - Modify:
whilly/api/templates/index.html.j2 -
Test:
tests/integration/test_htmx_dashboard.py - Step 1: Write failing dashboard parity test
Assert:
assert "p=pause workers" in body
assert "R=resume workers" in body
assert "Pause workers" in body
assert "Resume workers" in body
assert "togglePolling" not in body
assert "pollingPaused" not in body
- Step 2: Run dashboard test to verify failure
Run: .venv/bin/python -m pytest -q tests/integration/test_htmx_dashboard.py --maxfail=1
Expected: FAIL because WUI still exposes refresh pause.
- Step 3: Implement WUI parity
Add control state to OperatorSnapshot, render cluster state, add Pause/Resume buttons, map hotkeys p and R to admin endpoints, keep r as refresh, and remove local refresh-freeze logic.
- Step 4: Run dashboard test
Run: .venv/bin/python -m pytest -q tests/integration/test_htmx_dashboard.py --maxfail=1
Expected: PASS.
- Step 5: Commit
Run:
git add whilly/operator_views.py whilly/api/dashboard.py whilly/api/templates/index.html.j2 tests/integration/test_htmx_dashboard.py
git commit -m "feat(operator): align web pause controls"
Task 6: TUI Parity
Files:
- Modify:
whilly/cli/tui.py -
Test:
tests/unit/test_tui.py - Step 1: Write failing TUI parity tests
Assert:
assert "p=pause workers" in rendered
assert "R=resume workers" in rendered
assert "p=pause " not in rendered
handle_tui_key(state, "p")
assert state.pending_control_action == "pause"
handle_tui_key(state, "R")
assert state.pending_control_action == "resume"
- Step 2: Run TUI test to verify failure
Run: .venv/bin/python -m pytest -q tests/unit/test_tui.py --maxfail=1
Expected: FAIL because TUI still toggles local paused.
- Step 3: Implement TUI parity
Replace local paused state with pending_control_action. Apply actions through repository methods during the poll loop. Show WORKERS PAUSED when snapshot control state is paused. Keep r as refresh and never stop polling while workers are paused.
- Step 4: Run TUI test
Run: .venv/bin/python -m pytest -q tests/unit/test_tui.py --maxfail=1
Expected: PASS.
- Step 5: Commit
Run:
git add whilly/cli/tui.py tests/unit/test_tui.py
git commit -m "feat(operator): align tui pause controls"
Task 7: Review Decision Parity
Files:
- Modify:
whilly/cli/tui.py - Modify:
whilly/api/templates/index.html.j2 - Test:
tests/unit/test_tui.py -
Test:
tests/integration/test_htmx_dashboard.py - Step 1: Write failing review parity tests
Assert WUI/TUI both:
assert "j/k=select" in rendered
assert "a=approve" in rendered
assert "x=reject" in rendered
assert "c=changes" in rendered
And TUI records stage_id, reviewer, and decision source consistently.
- Step 2: Implement only Compliance-surface activation
WUI j/k/a/x/c should no-op unless Compliance is selected, matching TUI.
- Step 3: Run review parity tests
Run: .venv/bin/python -m pytest -q tests/unit/test_tui.py tests/integration/test_htmx_dashboard.py --maxfail=1
Expected: PASS.
- Step 4: Commit
Run:
git add whilly/cli/tui.py whilly/api/templates/index.html.j2 tests/unit/test_tui.py tests/integration/test_htmx_dashboard.py
git commit -m "feat(operator): align review hotkeys"
Task 8: Verification And Push
Files:
-
All changed files
-
Step 1: Run focused verification
Run:
.venv/bin/python -m pytest -q \
tests/unit/test_control_state_repository.py \
tests/unit/test_transport_schemas.py \
tests/unit/test_tui.py \
tests/unit/test_local_worker.py \
tests/unit/test_remote_worker.py \
tests/integration/test_control_state_admin_api.py \
tests/integration/test_htmx_dashboard.py \
--maxfail=1
Expected: PASS.
- Step 2: Run repository checks
Run: make test
Expected: PASS, or record exact failing test if the full suite is blocked by Docker/network.
- Step 3: Push
Run: git push origin main
Expected: remote main receives all phase commits. No merge is required when already working on main.
Self-Review
- Spec coverage: covers global pause/resume DB state, admin API, worker enforcement, WUI/TUI hotkey parity, and review hotkey parity.
- Placeholder scan: no TBD/TODO placeholders.
- Type consistency: repository returns
ControlState; transport exposesControlStateResponse; operator snapshot carries control state for WUI/TUI rendering.