Skip to content

fix: terminal deny presets, batch cancel token; feat: cockpit run context and heartbeat progress - #98

Merged
johnteee merged 8 commits into
mainfrom
claude/epic-wozniak-i1v4l1
Oct 27, 2026
Merged

johnteee merged 8 commits into
mainfrom
claude/epic-wozniak-i1v4l1

Conversation

@johnteee

Copy link
Copy Markdown
Member

Summary

Why: after #97, four read-only decision packets re-checked the remaining "owner decision" items against code and the project's own records. Two of them were already decided:

  • Deny presets: the owner chose "block in all modes" (docs/reviews/dogfood-g1-g22-2026-09-15-risk.md:22). docs/cli.md:923 and the permission playbook say the same. In prompt mode the code still offered a deny-matched call for interactive approval, so this is a defect, not an open question.
  • Cockpit and background lifecycle: DR-006 already carves this work out under owner-override: co-maintainer dogfood (docs/strategy/dr-006-owner-decision-2026-06-22.md:35). That covers DU-011 and DU-014.

The packets also found that the WS2-002 residual was cheaper than I had reported. The runner already has a cancel_token (teaagent/runner/_core.py:167,907). The subagent batch just never set it.

What:

Action Change Gate Test evidence
G-P1-4 POLICY_DENIED now goes to record_blocked and re-raises before any interactive approval (teaagent/runner/_governed_execution.py). approval why-denied describes policy_denied instead of "Unknown denial reason" governance-gap tests/test_deny_preset_runtime_enforcement.py. The handler is never called and the tool never runs. Falsifier: approval revoke <grant_id> lets the same approving handler run the tool, so revoke is the escape hatch
G-P1-5 Each subagent batch creates its own cancel_token, passes it to the children, and sets it at the deadline and on every exit. The parent's cancel still propagates. The runner re-checks the token before each tool dispatch (+3 lines in _core.py) governance-gap tests/test_subagent_batch_cancel_token.py. After the batch returned, 4 more tool calls ran before the fix and 0 after; the bound is at most 1 call already in flight. Cancellation is audited as the existing run_failed (system)
U-P1-7 (DU-011) ControlCockpitState and the TUI cockpit show branch, undo availability, last run id and active rules. Each source falls back to an explicit unknown, and the failure is logged owner-override (M4 carve-out) tests/test_control_cockpit.py: 6 new tests, including no-git, no-runs and failure cases
U-P1-8 (DU-014) Heartbeat ticks carry tool, tool_in_flight, elapsed_seconds, cost_cents and checkpoint through an allow-listed progress provider. heartbeat.py does not import runner internals owner-override (M4 carve-out) tests/test_heartbeat_progress_fields.py: 5 unit tests plus one real-run test
Docs New action-register rows. Backlog: the four items above are done, and PHASE-01…06 plus DU-008/010/015/017/018/020 get PROPOSED verdicts (1 close-as-superseded, 11 holds; not ratified). Roadmap-status has a dated update. Acceptance count is 674 — Docs consistency, OKF and aging checks pass

How: four worktree-isolated agents, each limited to its own files and to writing the test first. The coordinator reviewed every production diff, verified the agents' key claims against the code (the owner decision, the carve-out text, cancel_token wiring), cherry-picked the commits, and ran every CI lint and governance step plus the full suite locally.

Done: see below.

Not changed (noted for the owner)

  • Protected-path codes: WORKSPACE_WRITE_MODE, SYMLINK_BLOCKED and SKILL_WRITE_BLOCKED still reach the interactive approval prompt for destructive tools in prompt mode. That looks intended, since prompt mode means "ask", but no owner decision on record pins it.
  • DU-014 limits: checkpoint records the last completed call, because the runner has no "next checkpoint" concept. The liveness file still holds only the tick and the interval.
  • DU-011 private import: the cockpit imports the private _grant_expired helper.
  • Pre-existing failure: tests/test_hybrid_approval_queue_performance.py::test_large_scale_queue_performance fails on main too. It is a wall-clock assertion (22s against a 15s limit) and unrelated to this PR.

Action ID

Action: G-P1-4, G-P1-5, U-P1-7, U-P1-8 (registered in docs/retrospective/06-action-register.md in this PR)

Risk Class

Risk class: medium

This PR changes approval behavior in one direction only. A deny that already matched now always blocks, which is what the docs and the owner decision already promise. It also cancels subagent work at the batch deadline. The cockpit and heartbeat changes are additive observability.

Self-Review Checklist

General (all PRs)

  • ruff check + ruff format --check pass (locked ruff 0.15.20)
  • mypy teaagent/ tests/ --explicit-package-bases reports 0 issues
  • python3 scripts/run_test_tier.py --tier smoke passes (inside the full suite)
  • Coverage at least 75% (CI reported 79% on fix: governance-gap follow-ups (tick cost cap, batch timeout, deny usage error) + telemetry CI fix #97)
  • check_root_module_count.py ≤ 184
  • check_complexity.py ≤ 99 (62)
  • No circular imports
  • Event-spine wiring passes
  • Docs consistency passes
  • PR has Why / What / How / Done sections

High-Risk (if risk class = high)

  • n/a (medium)

Documentation

  • No contradiction with current-truth docs: this PR removes one, since prompt-mode deny override contradicted docs/cli.md:923
  • Commands in docs are executable
  • Stale dated docs updated or marked as superseded

Verification notes

  • Full suite at 4d826103 (locked .venv): 6830 passed, 34 skipped, 0 failed.
  • CI steps run locally: all 17 lint and governance-gate steps exit 0. That covers docstrings, config access, complexity, Any count, ruff, ruff format, mypy, bandit, test-quality severe and untyped, assertion regression, god modules, circular imports, event spine, root module count, selftest and tool lint.
  • Commit gates: every commit passes .githooks/commit-msg, check_dr006_gate_trailer and check_action_register_link.
  • Size: runner/_core.py grew from 1,062 to 1,065 lines; god-module exemptions are unchanged at 18.

Generated by Claude Code

Register the four follow-ups found by the 2026-10-27 decision packets so
their commits cite real action IDs:
- G-P1-4: deny presets must be terminal in prompt mode (owner decision
  "block in all modes", dogfood G1 risk record)
- G-P1-5: wire a per-batch cancel token into subagent batches (WS2-002
  residual)
- U-P1-7 / U-P1-8: cockpit state fields (DU-011) and heartbeat fields
  (DU-014) under the DR-006 M4 cockpit/background-lifecycle carve-out

Action: G-P1-4
Constraint: register rows only; statistics total updated from 63 to 67
Tested: validate_docs_consistency, docs inventory/aging/OKF regeneration
Confidence: high
Roadmap-Status: unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
Each heartbeat tick now carries run progress when a provider is wired:
tool (most recent tool_call_started), tool_in_flight, elapsed_seconds
(since the heartbeat was set up), cost_cents (engine usage), and
checkpoint ({call_id, at} of the last completed tool call, or null when
no checkpoint store is configured).

There is no next-checkpoint concept in the existing code: the checkpoint
store saves after each tool completion, with no id or schedule, so the
last checkpoint is recorded instead. Provider keys are restricted to the
allow-listed progress fields so it cannot overwrite tick or interval. A
failing provider marks progress_unavailable and does not stop the tick.
Without a provider the payload is unchanged (tick, interval_seconds).

Action: U-P1-8
Gate: owner-override (co-maintainer dogfood; DR-006 M4 background-lifecycle carve-out, dr-006-owner-decision-2026-06-22.md:35)
Constraint: Liveness file payload and runner/_core.py are unchanged; progress is audit-only. No next-checkpoint field because none exists.
Tested: pytest tests/test_heartbeat_progress_fields.py tests/test_heartbeat.py tests/acceptance/test_background_attach_resume_notify_flow.py tests/test_chat_agent.py -> 42 passed; pytest tests/acceptance -k "background or heartbeat or progress" -> 30 passed; pytest tests/test_chat_agent.py tests/test_cli.py tests/test_tui.py -> 194 passed; all pre-commit gates exit 0
Confidence: high
Roadmap-Status: unchanged
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
The control cockpit now reports the run context an operator needs before
acting: the checked-out branch, whether the latest undoable run can be
undone, the most recent run id, and the non-expired approval grants.
Each field reuses an existing source (git via _run_git, RunStore,
UndoJournal, ApprovalPresetStore) rather than new state.

Every field is best-effort. Absent data shows 'none'; a failed source shows
'unknown' and records a reason in errors and the log, so a broken store
never crashes the cockpit or passes as an empty result.

Action: U-P1-7
Gate: owner-override (co-maintainer dogfood; DR-006 M4 operator-cockpit carve-out, dr-006-owner-decision-2026-06-22.md:35)
Constraint: Only cockpit.py, the TUI state-panel renderer (tui/core.py), and the two cockpit test files changed. Undo availability and last run come from the run store, not from the separate RecoverableState path. Grants are read with readonly=True, and expired grants are filtered with the existing _grant_expired helper.
Tested: pytest tests/test_control_cockpit.py tests/acceptance/test_cockpit_snapshot_flow.py tests/acceptance/test_daily_cockpit_parity_flow.py tests/test_cockpit.py tests/test_cockpit_acceptance_spec.py tests/test_cockpit_run_status.py tests/tui tests/test_silent_exception_cleanup.py: 84 passed. New tests failed before the change (6 failed) and pass after (23 passed in the two primary files). All gates in gates.md exit 0.
Confidence: high
Roadmap-Status: unchanged
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
…ancel token

The subagent batch returned at its deadline and labelled still-running
children "result discarded", but the child kept executing tool calls.
Children inherited the parent's cancel token and the batch never set one,
so the abandoned thread ran on until its own iteration budget ended.

Each batch now creates one cancel token and passes it to every child via
run_subagent(cancel_token=...) -> _build_subagent_config. The batch sets it
at the deadline and on every exit path, and propagates a parent cancel
token that is set while the batch waits. AgentRunner gets one extra cancel
check immediately before tool dispatch, so a child whose decide() call
was in progress cannot start a new tool call after the deadline. Cancel
is recorded through the existing RunCancelledError -> run_failed path
(category system), so no new audit event type is introduced.

Action: G-P1-5
Gate: governance-gap (batch timeout results must be truthful; AGENTS.md Runtime Safety)
Constraint: no new cancellation framework; an in-flight tool call may finish; run_subagent and _build_subagent_config keep their default behaviour for callers that pass no cancel_token; runner/_core.py grows by 3 lines only
Tested: pytest tests/test_subagent_batch_cancel_token.py (2 new, failed before fix with 4 post-deadline tool calls, pass after with delta 0); tests/test_subagent_batch.py, tests/test_subagent_batch_timeout_running.py pass; pytest tests/test_subagent*.py tests/runner tests/acceptance/test_cancel_flow.py tests/acceptance/test_subagent_*.py tests/integration/test_cancel_token.py pass (265+ tests); all 11 gates in gates.md exit 0; check_god_modules passes (runner/_core.py 1062 -> 1065 lines)
Confidence: high
Roadmap-Status: unchanged
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
A matched deny preset raised ToolPermissionError(POLICY_DENIED) in the
approval manager, but the governed-execution except branch offered every
ToolPermissionError to the approval handler for destructive tools. In
prompt mode an approving handler therefore ran a call the owner denied.

Deny presets now short-circuit in the runner: POLICY_DENIED is recorded
as tool_call_blocked (reason_code policy_denied) and re-raised without
calling handle_approval_request. Removing the grant (approval revoke
<grant_id>) is the only way to let the call run again.

approval why-denied now describes policy_denied instead of printing
"Unknown denial reason".

Action: G-P1-4
Gate: governance-gap (owner decision "block in all modes"; docs/cli.md:923)
Constraint: other reason codes unchanged; JIT_NO_APPROVAL, MULTISIG_NO_QUORUM and protected-path codes (WORKSPACE_WRITE_MODE, SYMLINK_BLOCKED, SKILL_WRITE_BLOCKED) still reach the approval handler as before
Tested: pytest tests/test_deny_preset_runtime_enforcement.py tests/*approval* tests/approval/ tests/test_governance_adversarial_runtime.py tests/policy/ tests/acceptance/test_policy_denial_reason_code_flow.py tests/acceptance/test_approval_ux_parity_flow.py -> 358 passed (test_large_scale_queue_performance fails on base too, excluded); tests/runner tests/test_cli_ergonomics_handlers.py tests/test_acceptance_tier_runner.py -> 55 passed; all gates exit 0
Confidence: high
Roadmap-Status: unchanged
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
- backlog: DENY-OVERRIDE, the WS2-002 residual, DU-011 and DU-014 done;
  PROPOSED verdicts for PHASE-01..06 and DU-008/010/015/017/018/020
  (one close-as-superseded, eleven holds), with corrected DU-010/017 states
- action register: G-P1-4, G-P1-5, U-P1-7, U-P1-8 marked done
- roadmap-status: dated update; the G1 entry no longer says prompt mode
  can override a deny
- acceptance count 673 -> 674 (new cockpit snapshot assertion), via
  scripts/build_acceptance_status.py --source collect
- regenerate docs inventory, aging dashboard and all three OKF bundles

Action: G-P1-4
Constraint: docs and generated files only; PROPOSED verdicts are not ratified
Tested: validate_docs_consistency, refresh_competitive_docs --check, OKF checks; docs/okf/aging/inventory/acceptance-count tests 108 passed; tests/acceptance 670 passed, 3 skipped before the count update
Confidence: high
Roadmap-Status: updated the 2026-10-27 dated entry and the G1 caveat only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
Comment thread tests/test_control_cockpit.py Fixed
review-institution G4 failed: the PR touches teaagent/runner/_core.py,
a listed high-risk path, and docs/reviews/98-risk.md was missing. The
report covers all four changes, including the prompt-mode deny change in
_governed_execution.py, which is not on the path list but changes
approval behavior: blast radius, failure modes, dry run, per-commit
rollback and what stays with the owner. Restate the G5 corpus counts
for the added file.

Action: G-P1-4
Constraint: docs only; no code or test changes
Tested: check_review_institution_gate with PR_NUMBER=98 and the PR head; validate_docs_consistency; docs/okf/aging/inventory/high-risk-path tests 106 passed
Confidence: high
Roadmap-Status: updated the G5 corpus counts only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
github-code-quality flagged implicit string concatenation in a list at
tests/test_control_cockpit.py:415. The two literals are one JSON tool
call split across lines (the list has two items as intended); wrap them
in parentheses so the concatenation is explicit.

Action: U-P1-7
Constraint: test literal grouping only; no behavior change
Tested: ruff check, ruff format --check, mypy tests/test_control_cockpit.py, tests/test_control_cockpit.py 20 passed
Confidence: high
Roadmap-Status: unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
@johnteee
johnteee merged commit bf21c9e into main Oct 27, 2026
23 checks passed
johnteee pushed a commit that referenced this pull request Oct 27, 2026
U-P1-7 (#98) imported the private _grant_expired helper into cockpit.py.
Add ApprovalGrant.is_expired(), matching the is_expired() idiom already
used by governance/rbac.py and consensus/types.py, and call it from the
cockpit. Behavior is unchanged.

Action: U-P1-7
Constraint: no behavior change; _grant_expired keeps its other callers
Tested: tests/test_approval_grant_expiry.py (4 new), tests/test_control_cockpit.py, tests/acceptance/test_cockpit_snapshot_flow.py: 27 passed; ruff, ruff format, mypy on changed files
Confidence: high
Roadmap-Status: unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
johnteee pushed a commit that referenced this pull request Oct 27, 2026
- do-not-claim: replace "batch timeout/cancel is incomplete" with the
  accurate limit (no new tool call after the deadline; an in-flight call
  may finish)
- remote multi-agent non-goals: mark the WS2-002 row closed by G-P1-5
- audit-events: list the heartbeat progress fields
- operator cockpit contract: note that the TUI cockpit now reports undo
  availability (U-P1-7); per-file undo listing is still absent
- action register: regenerate the status summary from the table (67 done,
  G-P2-21 in progress) and the statistics (P1 row was missing; total 72,
  not the 67 written on 2026-10-27)

Action: G-P1-5
Constraint: docs and generated files only
Tested: validate_docs_consistency; docs/okf/aging/inventory/tier/guard tests pass
Confidence: high
Roadmap-Status: unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
johnteee pushed a commit that referenced this pull request Oct 27, 2026
main CI has been red after every GitHub-UI merge (#96, #97, #98): on a
push event the Lore-trailer gate read github.sha, the generated "Merge
pull request" commit, which never carries Constraint/Tested/Confidence.
The pull_request path already resolves to the PR head for the same
reason. For a pushed merge commit, check its second parent (the merged
PR head, which passed the gate on the PR). Single-parent pushes,
including web-UI edits, are still checked on their own message.

Simulated with the real history: 34b9643 (#99 merge) -> fc7dec8 and
bf21c9e (#98 merge) -> afe37a9 both pass .githooks/commit-msg; the
other cause of red main, the telemetry test (run 893), was fixed in #97.

Action: G-P2-21
Gate: governance-gap (CI must evaluate the authored commit; red main hides real failures)
Constraint: one workflow step; no gate weakened; merge commits only
Tested: workflow YAML parses; tests/test_ci_governance_flow.py and workflow tests 79 passed; shell resolution simulated on merges 34b9643 and bf21c9e
Confidence: high
Roadmap-Status: unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE5Q9zqTjf7w2PzjzAC2Qq
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.

2 participants