Repository navigation
fix: terminal deny presets, batch cancel token; feat: cockpit run context and heartbeat progress - #98
Merged
Merged
Conversation
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
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
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
13 tasks done
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
docs/reviews/dogfood-g1-g22-2026-09-15-risk.md:22).docs/cli.md:923and the permission playbook say the same. Inpromptmode the code still offered a deny-matched call for interactive approval, so this is a defect, not an open question.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:
POLICY_DENIEDnow goes torecord_blockedand re-raises before any interactive approval (teaagent/runner/_governed_execution.py).approval why-denieddescribespolicy_deniedinstead of "Unknown denial reason"governance-gaptests/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 hatchcancel_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-gaptests/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 existingrun_failed(system)ControlCockpitStateand the TUI cockpit show branch, undo availability, last run id and active rules. Each source falls back to an explicitunknown, and the failure is loggedowner-override(M4 carve-out)tests/test_control_cockpit.py: 6 new tests, including no-git, no-runs and failure casestool,tool_in_flight,elapsed_seconds,cost_centsandcheckpointthrough an allow-listed progress provider.heartbeat.pydoes not import runner internalsowner-override(M4 carve-out)tests/test_heartbeat_progress_fields.py: 5 unit tests plus one real-run testHow: 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_tokenwiring), 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)
WORKSPACE_WRITE_MODE,SYMLINK_BLOCKEDandSKILL_WRITE_BLOCKEDstill reach the interactive approval prompt for destructive tools inpromptmode. That looks intended, since prompt mode means "ask", but no owner decision on record pins it.checkpointrecords the last completed call, because the runner has no "next checkpoint" concept. The liveness file still holds only the tick and the interval._grant_expiredhelper.tests/test_hybrid_approval_queue_performance.py::test_large_scale_queue_performancefails onmaintoo. 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.mdin 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 --checkpass (locked ruff 0.15.20)mypy teaagent/ tests/ --explicit-package-basesreports 0 issuespython3 scripts/run_test_tier.py --tier smokepasses (inside the full suite)check_root_module_count.py≤ 184check_complexity.py≤ 99 (62)High-Risk (if risk class = high)
Documentation
docs/cli.md:923Verification notes
4d826103(locked.venv): 6830 passed, 34 skipped, 0 failed.lintandgovernance-gatesteps 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,selftestandtool lint..githooks/commit-msg,check_dr006_gate_trailerandcheck_action_register_link.runner/_core.pygrew from 1,062 to 1,065 lines; god-module exemptions are unchanged at 18.Generated by Claude Code