Resolve task_capability_map.py by unioning #850 bootstrap_author_issue_worktree
preflight transitions with master's #860 dirty-orphan recovery and work_issue
commit transitions. No feature logic discarded.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Child of umbrella #655 (governed MCP restart coordination); builds on the
#657 restart-path inventory. Adds a central coordinator that evaluates live
control-plane state before a restart and returns a blast-radius impact
preview, so operators and the web console (#642/#652) can see what a restart
would disrupt before concurrent LLM work is destroyed.
Changes
- restart_coordinator.py (new) — pure classification: inventory -> impact
report DTO (RestartImpactReport/SessionImpact/LeaseImpact). Verdicts:
safe / unsafe / override. Never restarts anything; fails closed on an
incomplete inventory.
- control_plane_db.py — additive ControlPlaneDB.list_sessions() read-only
session inventory (the process-level unit a restart kills).
- gitea_mcp_server.py — new dry-run MCP tool gitea_request_mcp_restart:
gathers sessions/leases/terminal-lock from the #613 DB, calls the
coordinator, returns the report. Override authority is read from the
environment, never self-asserted (#630/#710 F1 pattern). Apply is gated
by a later drain proof (non-goal here).
- docs/mcp-restart-coordinator.md + docs/mcp-restart-impact-sample.json — doc
and a real dry-run sample report.
- docs/mcp-tool-inventory.md — register the new tool (inventory sync).
- tests/test_restart_coordinator.py (new) — 15 tests: multi-session fixtures,
deny-when-critical-section-open, fail-closed deny, override, terminal lock,
stale heartbeat, JSON-serializable DTO, list_sessions.
Tests: pytest tests/test_restart_coordinator.py -> 15 passed. Full suite:
13 failed / 4753 passed; all 13 reproduce identically on clean master
@ef14622 (0 regressions). The residual test_issue_781 doc-registry failure
is a pre-existing baseline gap for gitea_rebind_dirty_same_claimant_author_session
(merged #864, undocumented on master) — out of scope for #658.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds a gated `system.restart_namespace` action so operators and workers can
restart or gracefully reload MCP namespaces through an authorized, audited
path instead of manual host process killing (#630).
- webui/sanctioned_restart.py: restart/reload operation model, dry-run
intent preview, confirmation-string enforcement, audit emission, and
post-restart health verification. Fails closed on unknown auth, missing
capability, or ambiguous target namespace.
- webui/console_authz.py: RBAC entries for the restart capability with
secret redaction preserved.
- webui/gated_actions.py: registers the restart action in the gated action
framework so it cannot be invoked without capability + confirmation.
- task_capability_map.py: capability mapping for the restart operation.
- docs/sanctioned-restart-controls.md: operator documentation for the
sanctioned path and the explicit prohibition on pkill recovery.
- docs/webui-authz-audit.md: audit model updated for restart events.
- tests/test_webui_sanctioned_restart.py: authorized preview, unauthorized
deny, confirmation enforcement, contamination classification, and audit
emission coverage.
No unrestricted kill path is exposed; manual pkill remains classified as
contamination and continues to block clean claims.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Resolve the #638 shell landing against the #639 dashboard:
- webui/layout.py: drop the flat NAV_ITEMS tuple in favor of master's
grouped NAV_GROUPS nav-config module.
- webui/nav.py: register /system-health as a live item in the Health
group, satisfying issue #639 AC5 through the canonical nav source.
- docs/webui-local-dev.md: keep both additive sections (#638 shell and
#639 dashboard).
- tests/test_webui_system_health_dashboard.py: assert the nav entry via
iter_nav_items() instead of the removed NAV_ITEMS tuple.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The Related section and cross-reference lines described #652, #653, #630, #642,
and #591 by roles they do not hold. Align each description with the linked
issue's actual title and scope:
- #652 is the Control Plane Web Console product vision (restart controls live in
its capability area A), not a restart-specific vision.
- #653 is the console phased-delivery roadmap; restart controls are Phase 2.
- #630 is the manual process-kill contamination guard, not the coordinator; the
coordinator remains an unimplemented later child of #655.
- #642 is the sanctioned restart / graceful reload console UX.
- #591 is auto-restart on master advance (closed); only #584 is transport-flap
reconnect. They were previously collapsed into one transport-recovery label.
Documentation-only wording change. Policy IDs RG-01..RG-08, the policy version
restart-governance/v1, the authorization matrix, and every normative statement
are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds docs/architecture/mcp-restart-governance.md, the restart-governance/v1 ADR
defining who may restart the MCP control plane and under what conditions.
- Recovery ladder (reconnect -> rebind -> scoped restart -> full restart -> host)
with restart stated as the last resort.
- Authorization matrix across author/reviewer/merger/reconciler/controller/
operator/admin; no LLM worker role may perform or authorize a full or host
restart.
- v1 authority decision recorded: controller approval + automated safety gates;
quorum deferred to a superseding ADR.
- Break-glass path with pre-declared incident and mandatory post-hoc audit.
- Ambiguous policy state denies restart.
- Stable policy IDs RG-01..RG-08 for later enforcement code to bind to.
Cross-links the ADR from docs/safety-model.md and docs/webui-deployment.md, and
adds tests/test_mcp_restart_governance_docs.py asserting acceptance criteria 1-5.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Remediates the two blocking findings in review #522 on PR #849.
F1 — the session filter dimension was dead end to end. No source could
produce an event carrying a session identifier, so filter_events dropped
every event whenever session was supplied and the API answered with a
green, empty page. An empty result reads to an operator as "no such
session activity", which is a stronger and false claim.
Each source now declares which filter dimensions its records can actually
carry. The control-plane events table is (event_id, work_item_id,
event_type, message, created_at) and records no session, so that source
declares the session dimension unsupported rather than pretending to
answer it; the dead read of a non-existent session_id column is removed.
A CTH handoff comment declares its own Session field, so the handoff
adapter populates session_id from that declared field — authoritative
source data, never inferred from an actor, work item, or message text.
When no source that ran can carry a requested dimension, load_timeline
refuses with ok=false and a structured error naming the unsupported
filters and the per-source reason, and the route answers 422. A source
that can answer the dimension and simply matched nothing still returns
200 with an honest empty page. Ordering, pagination, and the issue/PR
filters are unchanged.
F2 — evidence_refs bypassed redaction and could emit a credential
verbatim. proof and decision were passed to _extract_evidence_refs before
redaction, and the SHA pattern matched any 7-40 character lowercase hex
run, which is exactly the shape of a Gitea access token.
Redaction now runs first and every derived value is taken from the
redacted text. A commit reference is recognised only where the source
text declares one (commit, head, base, sha, ...), so an undeclared hex
run is never lifted out of prose into a structured field; this also drops
the ordinary-word noise the reviewer noted. Every reference is then
independently revalidated against an allowed shape and a second redaction
pass immediately before serialization, failing closed by dropping
anything unproven and flagging the event sensitive. Actor is redacted for
the same reason, and a secret-shaped session value is dropped rather than
emitted. Legitimate issue, PR, short-SHA and full 40-character SHA
references stay usable.
Tests: 16 added. Session filtering is now driven through the CTH adapter
and the composed load_timeline/API path rather than a hand-built
WorkflowEvent, covering a match, an honest empty result, pagination and
ordering under the filter, the 422 refusal, and the per-source support
declaration. Redaction coverage asserts a synthetic 40-character hex
value (not a real credential) appears nowhere in the complete serialized
payload including evidence_refs, that the independent revalidation drops
unproven references, and that legitimate references still resolve.
pytest tests/test_webui_timeline.py: 42 passed (was 26).
pytest -k webui: 340 passed, 270 subtests passed (was 324).
Full suite: 4607 passed, 12 failed, 6 skipped, 684 subtests passed — the
same 12 failures as the master baseline, none under webui/.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Phase 1 child of the Web Console epic #631. Adds the operator-facing
system-health dashboard on top of the read-only system-health API landed
by #634, so runtime problems are visible on a surface instead of being
discovered late through failed LLM sessions.
- webui/system_health_views.py (new): renders the SystemHealthSnapshot as
readiness, stale-runtime parity, version/uptime, dependency, MCP
namespace, probe-error, and recovery cards.
- webui/app.py: GET /system-health, sharing load_system_health() with the
JSON API so page and API cannot disagree. ?deep=1 behaves as on the API.
- webui/layout.py: nav entry and health card/badge styles.
- tests/test_webui_system_health_dashboard.py (new, 26 cases).
- docs/webui-local-dev.md: route, field authority, and redaction split.
Readiness honesty is preserved from the API: ready and readiness_complete
render separately, a probe that did not run is listed under "Not probed"
rather than counted healthy, and mutation safety is never claimed when the
runtime is stale or parity is indeterminate.
Redaction is split by field kind. Free text (probe details, reasons, probe
errors) passes through system_health.redact. Structured fields (commit
SHAs, probe names, statuses, timestamps) are HTML-escaped only: redact's
opaque-token rule matches any run of 32 or more characters, so routing a
40-character git SHA through it rendered "[redacted]" and blanked the
parity evidence the page exists to show.
Non-goals honored: no restart or reload controls (Phase 2, #642), no
manual process-kill guidance (#630). Read-only throughout.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Phase 1 child of the Web Console epic #631. Adds a durable, versioned
WorkflowEvent schema with per-source adapters and a read-only query API so
operators can browse a unified timeline of workflow events, decisions, tool
calls, and handoffs instead of scattered evidence.
- webui/timeline.py (new): versioned WorkflowEvent schema; control-plane
event adapter and Gitea CTH handoff-comment adapter; read-only mode=ro
control-plane reader; conjunctive filter by issue/PR/session; stable
(timestamp, source_rank, event_key) ordering; bounded pagination;
fail-soft per-source status; redaction at the boundary, fail closed.
- webui/app.py: GET /api/v1/timeline read-only route with thread-scoped,
fail-soft handoff comment source.
- tests/test_webui_timeline.py (new): schema, adapters, redaction of
secret-like payloads, filter/sort/pagination, scoped CP reader,
fail-soft composition, and API integration.
- docs/webui-local-dev.md: timeline route and field-authority notes.
Read-only Phase 1; no mutation of historical events; no full chat replay;
no unredacted tool-argument storage.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Resolve conflict remediation for PR #818 (Closes#638) against master
caaae9b6. Two conflicts in webui/app.py, both resolved as unions since
the Phase 1 shell work (#638) and the merged master changes touch
disjoint concerns:
- Imports: keep the new webui.nav (NAV_GROUPS, STUB_PAGES) import from
#638 alongside master's expanded project_registry / project_views API
(ProjectRegistry, RegistryError, known_project_ids,
project_detail_to_dict, render_registry_error).
- Route table: keep master's read-only /api/console/security-model route
alongside #638's read-only Phase 1 stub routes (STUB_PAGES).
No behavior change beyond union; console stays read-only. docs/webui-local-dev.md
auto-merged. Full webui suite green (272 passed, 310 subtests).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Introduce a single nav-config module (webui/nav.py) driving grouped
navigation across the epic #631 Phase 1 information architecture:
Health, Traffic, Runtime/Sessions, Projects, Inventory, Timeline,
Policy (placeholder), and Insights (placeholder). The shell header now
carries a read-only environment badge (local/remote from WEBUI_HOST), a
mode: read-only badge, and a Docs link. Home summarizes the console
purpose, lists the Phase 1 surfaces by group, and links the MVP legacy
pages.
Not-yet-implemented surfaces (/sessions, /inventory, /timeline,
/policy, /insights) resolve to graceful read-only stub pages rather than
404s; their backing views land in later child issues of #631 (inventory
surfaces backed by #636). Mutating methods on stub routes still fail
closed with read-only-mvp. No privileged action controls are added.
Adds tests/test_webui_shell.py covering nav groups, badge presence,
environment classification, docs link, stub routes (200 + read-only),
and home content. Updates docs/webui-local-dev.md with the new routes
and a Phase 1 shell section.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Entry point B of #812 is the state where an author's work has already advanced to a local commit: the worktree is registered, clean, on the issue branch, and carries the only copy of the implementation, but the branch has never been published. Two individually correct predicates close a cycle around it: exact-owner lease renewal refuses without an observable remote head, and every publication path is lock-derived under #618, so nothing can create that remote head without first holding the lock renewal would grant.
This adds the missing operation. gitea_publish_unpublished_issue_branch publishes an already-committed local head to its remote branch, so exact-owner renewal has the evidence its model requires. Publication is the whole of its authority: it renews, reclaims, rebinds, and clears nothing.
Why this is not a lock bypass: the operation can only publish a branch whose durable issue-lock record already names the caller as claimant. Ownership is read from the lock file, never asserted by the caller. Guard strictness is unchanged (AC15): a dirty tree, an untracked file the commit does not carry, an unregistered worktree, a non-issue or stable branch, a changed local HEAD, a remote head that is not an ancestor of the commit, a competing open PR for the same issue on another branch, and any declared-hash mismatch each fail closed. The refspec names the commit SHA explicitly and never forces.
Record separation (AC23): the durable issue-lock file and the control-plane workflow lease are distinct records. This reads the former as ownership evidence and writes neither.
Truthful process evidence (AC24): the recorded owner pid's liveness is never consulted or asserted. A regression pins the recorded pid to a live process, proves publication still succeeds, and proves expired-lock reclaim still refuses for that same pid.
Scope is AC20 only. AC21 cannot unblock on its own, so the two are separable and only the smaller one is implemented here.
Tests: 36 new cases against synthetic fixtures only, using a real git repository with a real local bare remote so publication and read-after-write verification are genuinely executed rather than mocked. AC17 is honoured and a regression asserts the protected worktree is never referenced.
Full suite: 11 failed, 4354 passed, 6 skipped, 533 subtests passed. The 11 failures are the documented pre-existing drift baseline at 9eb0f29, unchanged in count and identity.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds `GET /api/v1/system/health`, a structured read-only health surface for
automated readiness checks, and keeps `/health` as the cheap liveness probe.
webui/system_health.py composes a DTO from fail-soft dependency probes: the
control-plane database, the local checkout, and — opt-in via `?deep=1` — live
Gitea reachability, each carrying status, reason, and probe latency. Required
probes drive readiness; the optional Gitea probe can only degrade overall
status, because local inventory stays serveable when the remote is
unreachable. A probe that did not run leaves readiness incomplete rather than
silently passing.
Read-only throughout: the control-plane database is opened through a `mode=ro`
URI because `ControlPlaneDB.__init__` creates directories and runs migrations,
which a health check must never do. No restart or reload control is exposed;
those are Phase 2 and #630 forbids process-kill recovery.
No unproven claims: `stale_runtime.mutation_safe` is true only when the
runtime, checkout, and remote commits are all known and equal, and MCP
namespaces always report `unproven` because a web process cannot exercise the
IDE-managed client path (#543). Probe details are redacted at the browser
boundary — URLs lose userinfo and query strings, credential-shaped text is
masked.
`/health` is expanded additively: every MVP key is retained, plus `started_at`,
`uptime_seconds`, and a pointer to the versioned API. The versioned route
returns 503 when not ready so automation can branch on the status code alone.
Verified at master 9eb0f29: focused file 40 passed / 11 subtests; `-k "webui or
health"` 230 passed / 159 subtests; full suite 4358 passed with the 11
pre-existing master-drift failures unchanged from the clean-master baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Evolve the MVP project registry (#427) into a versioned, fail-closed
project registry API for the console (Phase 1, read-only).
- Add schema version 2 with project `status`, per-step onboarding
`state`/`required`, optional redacted `last_seen_health`, and
`remote_name`. Version 1 files stay loadable and are normalized with
explicit defaults.
- Serve `/api/v1/projects` and `/api/v1/projects/{project_id}` with API
provenance (`api_version`, `schema_version`, `source`). `/api/projects`
is retained as an unversioned Phase 1 alias.
- Replace bare `ValueError` with `RegistryError`, carrying an operator
`remediation` and `field_path`; invalid registries fail closed as a
500 JSON payload or a dedicated HTML error page instead of a traceback.
- Reject credential-shaped keys before any DTO is built, reusing
`registry_safety.is_forbidden_key` as the single source of truth
shared with the worker registry (#798).
- Render HTML views from `project_to_dict`, so the console and the JSON
API cannot disagree about status or onboarding progress.
- Document the contract in docs/webui-project-registry-api.md.
Tests: registry load/validate (valid, missing project, schema
validation, credential rejection) and API route coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>