Adds the durable architecture record epic #631 lacked: layer contract,
authority boundaries, the redaction boundary, /api/v1 versioning with a
compatibility rule for the existing unversioned MVP exports, a target page
map, per-child component ownership for all twenty children (#632-#651),
phase gates, and explicit forbidden paths.
Documentation only. No UI, API, or deployment change.
- docs/architecture/webui-control-plane-console-architecture-adr.md (new)
- docs/webui-local-dev.md: cross-link to the ADR
- tests/test_webui_architecture_docs.py: doc gate for the acceptance
criteria and the cross-link (12 cases)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Review #497 on PR #789 found that adding `&` to the separator alternation
created a new false-positive class: the splitter was quote-unaware, so any
quoted text carrying an ampersand and a kill verb classified as a manual
daemon kill. Three commands regressed against base 35e94e10, all of which
merely mention the canonical kill string:
git commit -m "block sleep 1 & pkill -f mcp_server.py as recovery"
echo "docs: sleep 1 & pkill -f mcp_server.py is now detected"
grep -rn "sleep 1 & pkill -f mcp_server.py" docs/
A false contamination marker fails review, merge, close and completion
mutations closed and only a reconciler may clear it, so this is an
operator-visible stall rather than a warning.
F1: replace the `_SEGMENT_SPLIT_RE` alternation with a quote-aware scan.
`_iter_active` yields only the character positions where a metacharacter is
syntactically active — outside single and double quotes, not backslash
escaped — and `_split_segments` separates only there. This fixes the class
rather than the three named instances, and also retires the `;` and `|`
false positives that predate #787. `&&` and `||` are still consumed whole,
so logical-separator precedence is unchanged.
F3: `_strip_subshell` now removes a trailing `)` only when it closes a
leading `(` this call stripped, so `kill $(pgrep -f myapp)` is no longer
mangled into `kill $(pgrep -f myapp`; and `_is_redirection` keeps `2>&1`,
`>&2` and `&>log` from being read as background separators. Neither changed
a verdict at the rejected head, but both are corrected here because the
splitter was reworked for F1.
Nine focused cases added: the three F1 commands, double-quoted, single-quoted
and backslash-escaped ampersands, the POSIX rule that a backslash does not
escape inside single quotes, the `;`/`|` class, the two F3 forms, and a
record-tool case asserting no marker is written for a quoted mention.
Focused suite 64 passed (47 at base 35e94e10, 55 at rejected head 6b58f04).
Full suite 11 failed, 4190 passed, 6 skipped; the same 11 failures occur
test-for-test at base and are unrelated to this branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
`runtime_recovery_guard._SEGMENT_SPLIT_RE` claimed to split a compound command
line "on shell separators", but omitted the background-job separator `&`, and no
caller stripped a subshell wrapper before tokenising. `_analyse_kill_segment`
only inspects the first command token of a segment, so in both forms the kill
verb never reached the classifier.
Measured against the previous implementation at 35e94e10:
sleep 1 & pkill -f mcp_server.py -> process_kill False, contamination False
(pkill -f mcp_server.py) -> process_kill False, contamination False
Changes:
- Add `&` to `_SEGMENT_SPLIT_RE` as `(?:\|\||&&|[|;&\n])`; the two-character
logical forms stay first so `&&` and `||` are consumed whole.
- Add `_strip_subshell()` and apply it in `_split_segments()`, removing leading
`(` and trailing `)` (including nested wrappers) before tokenising.
Both forms now classify as contamination with reason class `manual_daemon_kill`,
and the record tool writes a durable marker for each. Direct-command behaviour is
unchanged. The contamination model, gated-task set, marker lifecycle, and
reconciler-only clearing path are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A session that ran `pkill -f mcp_server.py`, waited for the IDE to respawn
the daemons, and then closed an issue left no trace distinguishing that
closure from one performed over a sanctioned runtime. Detection existed only
as advisory strings (`native_mcp_preference.classify_command_path`,
`review_workflow_boundary`) and never failed closed on the mutations that
followed.
Adds `runtime_recovery_guard.py`, mirroring the #671 stable-branch
contamination model so the two cannot drift apart: classification of
kill/pkill/killall commands and known-pid kills, a durable
`runtime_recovery_contamination` marker, a fail-closed pre-flight gate over
the shared review/merge/close/completion task set, and reconciler-only
clearing. `comment_issue` and `lock_issue` stay allowed so a contaminated
worker can still post its audit comment and hand off. The marker is
recovery-critical, so contamination cannot expire into cleanliness with the
session-state TTL.
Read-only inspection (`ps aux | grep mcp_server`), sanctioned reconnects,
and process management unrelated to the daemons are never flagged; a bare
`kill` of an unknown pid is reported as ambiguous rather than contaminating,
so ordinary subprocess work is not false-blocked. A pattern broad enough to
sweep unrelated namespaces (`pkill -f python`) is contamination even when it
never names MCP.
Operator-authorized host maintenance stays permitted, but the authorization
is read from GITEA_OPERATOR_DAEMON_MAINTENANCE_AUTHORIZATION in the process
environment only and never from a tool argument, so a session cannot
authorize itself.
Final-report rules reject clean-session claims and require the contaminated
recovery to be surfaced. Two tools are registered (inventory 112 to 114) and
the sanctioned-versus-forbidden contrast is documented in the namespace
recovery doc and the workflow skill.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds the canonical cross-role handoff schema and its fail-closed validator,
live-state recovery, role-limited continuation, mandatory durable posting,
the merged-awaiting-controller boundary, controller accept/reject
continuation, and workflow-failure escalation with duplicate handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Auto-mode classifier now distinguishes local validator rejection,
capability-gate rejection, transport failure before API, and successful
server-side mutation. Pre-API validator failures no longer consume
server-side mutation budget; the final report separately accounts for
local failed attempts, blocked API attempts, and successful server-side
mutations.
Recovered from preserved unpublished commit b46f0f9 via native MCP
unpublished-claim recovery (#772) and author-worktree lock binding (#618),
reconciled onto current master.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Closes#772
Recovery now dispatches on observed publication state: published_owning_pr
(remote branch exists; head equality #753 or strict descendancy #768,
unchanged) and unpublished_claim (no remote branch, no PR; ownership proven
by the durable lock record plus a local HEAD strictly descending from the
server-observed base the branch was cut from).
The absence of a remote head is never itself permission. Recovery writes are
compare-and-swap against a lock generation, and the mutating lock path and
read-only diagnostic assessor share one evaluator.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Recovered preserved candidate d06198b onto current master 0c2f45a via clean
cherry-pick. Active foreign leases are excluded before allocator ranking;
controller_instance_id ownership is persisted; dashboard matches allocator
exclusion. Stable patch-id bacafc5f… preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Author mutation tools now resolve workspace via explicit worktree_path,
env bindings, or the active author issue lock — never silent fallback to
the control checkout/master. Missing configured bindings fail closed with
operator recovery; create_issue and create_issue_comment agree.
Recovered onto 0568f44 from preserved candidate cbf56ccd (AUTHOR_RECOVERY).
LLM_LOCK_ID=author-618-recovery-508eb3162d01-1784569919
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Remediates review 479/481 findings F1 and F2 on PR #767.
F1: issue #607 AC4 requires that an existing linked Gitea issue receives a
recurrence comment when Sentry events continue. That path did not exist.
This adds:
- incident_bridge.incident_recurred() — true only when event_count or
last_seen genuinely advanced, compared against the pre-upsert link row, so
an unchanged rescan stays silent and the exactly-once property holds.
- incident_bridge.build_recurrence_comment_body() — reuses the same
redact_text path as build_gitea_issue_body, so redaction is not
re-implemented.
- A comment_issue_fn hook on reconcile_incident, invoked only on
OUTCOME_UPDATED with genuinely new events. The durable incident_links row
is written first, so a comment failure degrades to "link updated, comment
withheld" without corrupting the mapping or creating a duplicate issue.
- _incident_recurrence_comment_fn() in gitea_mcp_server.py, routing through
the sanctioned gitea_create_issue_comment path named in issue #607. It
returns None on dry runs and when the profile lacks gitea.issue.comment, so
the AC8 dry-run default and disabled-mode safety hold.
- comment_issue_fn threading through sentry_incident_bridge.watchdog(), with
the per-issue recurrence_comment outcome recorded in the scan result.
F2: observation_from_issue never populates a fingerprint, so the "deduped by
Sentry issue id and fingerprint" claim was unsupported. The
gitea_sentry_reconcile_issue and gitea_sentry_watchdog docstrings now state
the real dedupe basis: provider identity (provider, base URL, org, project,
Sentry issue id).
Tests: five new AC4 cases in tests/test_sentry_incident_bridge.py covering a
comment on the second scan, dry-run silence, no-new-events silence, link
durability when the comment fails, and comment redaction. Focused suite
43 passed (was 38).
Refs #607
Permit fail-closed recovery when a clean local head is a strict
descendant of the recorded PR/remote head, with server-side ancestry
proof. Propagate recovery evidence through commit, push, and PR
duplicate gates so an owning PR is not re-blocked as competing work.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Reviewed-on: #680
Reviewed-by: sysadmin <[email protected]>
Operator break-glass merge record
PR #680 was independently assessed at head `7c77df0c527f06ae4196e010e65537d361805a7b`.
* Gitea reports the PR open and mergeable.
* The focused anti-stomp suite passed: 43 tests.
* The technical review found the change approve-worthy.
* The previous approval is stale because it applies to an earlier head.
* A fresh formal approval could not be submitted because every MCP namespace remained bound to pre-#694 startup SHA `f65069d…`, while live master is `3e78db5…`.
This is an operator-authorized break-glass merge caused by stale MCP runtime parity, not an unresolved code finding. Proceeding with **Create merge commit** only if the PR head remains exactly `7c77df0c527f06ae4196e010e65537d361805a7b`, Gitea still reports it mergeable, and no new blocking checks or feedback appear.
Reviewed-on: #679
Reviewed-by: sysadmin <[email protected]>
Operator break-glass merge of PR #679 authorized because the native MCP
gitea_adopt_merger_pr_lease path returned non-retryable internal_error
despite an authoritative active reviewer lease. PR #679 was formally
approved by Review #446 at exact head
78cc37a977 and was conflict-free and
mergeable. No LLM received credentials or used a direct API/CLI fallback.
When gitea_lock_issue adopts an existing own branch, the live tool
response now carries explicit, citable adoption-proof fields so #473-style
recovery sessions can quote the lock output directly instead of inferring
adoption from separate offline checks.
- issue_lock_adoption.py: add DECISION_LABELS + decision_label()/
safe_next_action() helpers; extend build_adoption_proof() with
adoption_decision, adopted, adopted_branch, adopted_branch_head,
matcher_summary (boundary-safe reason), competing_branch_check, and
safe_next_action for all outcomes; add build_non_adoption_lock_proof()
for adoption-free NO_MATCH metadata.
- gitea_mcp_server.py: attach adoption-free adoption_check block to normal
(non-adopt) lock responses so they cannot be misread as claiming adoption.
- docs/llm-workflow-runbooks.md: document the adoption/adoption_check
response blocks and instruct recovery reports to cite them directly.
- tests: explicit-field proof for ADOPT/BLOCK_COMPETING/NO_MATCH,
substring-collision boundary safety (issue-42 vs issue-420), non-adoption
proof, and MCP-level live-response assertions.
BLOCK_COMPETING lock attempts still fail closed (raise); no raw API
fallback, branch deletion, force-push, or manual lock seeding introduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Recreation of the #204 work from closed PR #205 (invalid provenance), rebuilt
cleanly on master under the prgs author identity with no PR #203 content:
- Add gitea_lock_issue MCP tool: locks exactly one issue to its branch name,
fails closed on branch/issue-number mismatch and on issues already tied to
an open PR (by head branch or Closes/Fixes reference).
- gitea_create_pr now requires the issue lock: head must match the locked
branch, title/body must contain Closes/Fixes #<locked issue> exactly, and
ambiguous references (equivalent / related / same as) are rejected.
- scripts/worktree-start refuses to create an issue-linked worktree unless the
lock file exists and matches the requested branch.
- assess_controller_handoff rejects handoffs whose selected issue / opened PR
fields carry multiple numbers or fuzzy equivalence wording.
- Tests: TestIssueLocking (lock + create_pr gates), handoff exact-reference
tests, worktree-start lock coverage.
Closes#204
Co-Authored-By: Claude Fable 5 <[email protected]>