feat(control-plane): persist allocator dependency edges as durable state (Closes #784) #785
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#785
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #784 (Slice A of umbrella #628, scope item 6). Head
0589ec80698295e8a1e8f18974849000cf1f5861, based on master300e8acd.What was wrong
Umbrella #628 scope item 6 requires dependencies to be durable structured state carrying source, target, type, blocking condition, completion condition, current state, and evidence. Nothing stored any of that.
Dependency knowledge existed only as a per-run computation.
allocator_dependencies.parse_dependency_refs()re-parsed theDepends:declaration out of every issue body on every allocation;_allocator_candidates_from_gitea()resolved each reference against live issue state; the result collapsed intoWorkCandidate.dependency_unmet/dependency_reason, whichclassify_skip()consumed and discarded.control_plane_db.pyhad no dependency table at all.Three consequences:
What this adds
dependency_edgestable, schema v4. Creating the table is the v3→v4 migration: additive, idempotent, and it never touches the existing tables. Uniqueness is (scope, source, target, edge_type), so re-observing a relationship updates one row instead of appending duplicates.dependency_graph.pyowns the vocabulary — the seven #628 relationship types, the three states, and fail-closed normalization for types, states, and endpoint kinds. An unrecognized value writes nothing rather than landing as unqueryable free text. Evidence is sanitized before storage, so no credential or endpoint URL can be persisted or handed back.ControlPlaneDBmethods —upsert_dependency_edge,list_dependency_edges,record_dependency_edge_observation. Filtering by target makes reverse lookup a single query. Transitions append to the existingeventstable rather than a parallel audit table.met/unmet/unavailablepartitions map one-to-one onto stored states, so nothing is re-classified and unavailable evidence is never recorded as met.gitea_list_dependency_edges, read-only, gated ongitea.read, added to the documented inventory the #781 drift guard checks (111 → 112 tools).What this deliberately does not do
Selection is untouched.
classify_skip()still consumes the in-memorydependency_unmetfield; the durable store is written alongside it and never read by the selection path. The write is best-effort and reports failures throughreasons, so a broken or absent store leaves allocation behaving exactly as before — proven by allocating the same candidate set through a store whose writes all raise, then comparing selection, skip set, and candidate count.Automatic blocking and resumption (#628 item 7), edge creation from PR/review/merge events, and defect auto-linking (#628 item 9) are later slices. Umbrella #628 is neither claimed nor closed by this work.
Verification
tests/test_issue_784_dependency_edges.py: fresh-schema creation, a real v3→v4 migration with row retention across all seven pre-existing tables, idempotent re-migration, enum rejection writing nothing, upsert idempotence, forward and reverse lookup, scope isolation, transition events carrying prior and new state, redaction proven at rest in the raw column, live allocation-run ingestion, write-failure tolerance, and the tool's permission gate.test_commit_payloads,test_issue_702_review_findings_f1_f6,test_mcp_server,test_post_merge_moot_lease,test_reconciler_supersession_close) reproduce identically on a clean detached checkout of master at300e8acd— 11 failed, 254 passed. They are pre-existing and proven by baseline run, not asserted.Reviewer notes
events.work_item_idis NULL for edge transitions: an edge endpoint is a Gitea issue/PR that may never have been assigned, so it has nowork_itemsrow to reference._allocator_candidates_from_gitea()gained two optional keyword arguments; the dashboard call site passes no store because a read-only view should not write.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #785
issue: #784
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 8563-20950baee7f0
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-dependency-edges
phase: claimed
candidate_head:
0589ec8069target_branch: master
target_branch_sha:
300e8acd13last_activity: 2026-07-21T22:17:30Z
expires_at: 2026-07-21T22:27:30Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #785
issue: #784
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 8563-20950baee7f0
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-dependency-edges
phase: claimed
candidate_head:
0589ec8069target_branch: master
target_branch_sha:
300e8acd13last_activity: 2026-07-21T22:19:30Z
expires_at: 2026-07-21T22:29:30Z
blocker: none
Review decision: approve
Independent formal review of PR #785 (Closes issue #784, Slice A of umbrella #628), performed on the reviewer namespace against a detached review worktree pinned to the PR head. The reviewer session edited no file in this branch; every finding comes from reading the diff and executing the suite. The verdict below is an official review mutation recorded live through the reviewer API, not a draft and not a proposal held for later submission.
NATIVE_REVIEW_PROOF: transport=native_mcp; entrypoint=mcp_server; namespace=gitea-reviewer; profile=prgs-reviewer; identity=sysadmin
Workflow-load helper result: workflow_hash: 263d0a6cb8a6; boundary_status: clean; final_report_schema_hash: b6c65affc336; workflow_source: skills/llm-project-workflow/workflows/review-merge-pr.md; pre_review_command_count: 0
Reviewed head SHA:
0589ec8069Live head SHA before approval:
0589ec8069Pushes occurred during validation: no
Prior-landing gate: NOT_LANDED — the head commit exists only on the feature branch; master remains at
300e8acd13and carries none of these six files' changes, so this is original unlanded work rather than reconciliation of existing history.Baseline and validation evidence
Pre-merge base commit:
300e8acd13Tested commit:
0589ec8069Command: /Users/jasonwalker/Development/Gitea-Tools/venv/bin/python -m pytest tests -q -s -p no:randomly
Working directory: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-dependency-edges
Executable path proof: /Users/jasonwalker/Development/Gitea-Tools/venv/bin/python (project virtualenv interpreter, invoked by absolute path)
Exit status: 1
Failure signature: 11 failed, 4126 passed, 6 skipped, 493 subtests passed — failures confined to test_commit_payloads (6), test_issue_702_review_findings_f1_f6 (2), test_mcp_server (1), test_post_merge_moot_lease (1), test_reconciler_supersession_close (1)
Baseline execution at the pre-merge base commit, in a clean detached worktree created by this reviewer session:
Baseline worktree path: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-baseline
Pre-merge base commit:
300e8acd13Command: /Users/jasonwalker/Development/Gitea-Tools/venv/bin/python -m pytest tests/test_commit_payloads.py tests/test_issue_702_review_findings_f1_f6.py tests/test_mcp_server.py tests/test_post_merge_moot_lease.py tests/test_reconciler_supersession_close.py -q -s -p no:randomly
Working directory: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-baseline
Executable path proof: /Users/jasonwalker/Development/Gitea-Tools/venv/bin/python (same interpreter, absolute path)
Exit status: 1
Failure signature: 11 failed, 254 passed — the same eleven node ids, carrying the identical assertion text "Expected: verify_preflight_purity('prgs', task='reconcile_close_superseded_pr') / Actual: verify_preflight_purity('prgs', task='reconcile_close_superseded_pr', org='Scaled-Tech-Consulting', repo='Gitea-Tools')"
The failure sets at the tested commit and at the pre-merge base commit match in membership and in signature, so this branch introduces no new failing test. The author's numbers were re-executed by this session rather than accepted.
Scope verification against the linked issue
Six files changed, 1475 insertions, 5 deletions. Every change maps to an issue #784 acceptance criterion; nothing unrelated is touched.
Schema (AC1-AC3). control_plane_db.py moves SCHEMA_VERSION 3 to 4 and adds dependency_edges to the shared DDL block. The statement is CREATE TABLE IF NOT EXISTS inside the executescript that already runs on every open, so table creation is itself the migration: additive, idempotent, and structurally incapable of altering the seven pre-existing tables. Proven by test rather than by inspection: the test module inlines the prior v3 schema, seeds work_items, sessions, and events rows, opens the database, then asserts the version advances to 4, the new table appears, and every seeded row survives. Two further opens leave exactly one dependency_edges table.
Vocabulary and fail-closed input handling (AC4, AC5). dependency_graph.py declares all seven relationship types umbrella #628 enumerates plus the three observation states. normalize_edge_type, normalize_edge_state, and normalize_work_kind raise instead of defaulting. The database layer calls all four normalizers before opening a transaction, so a rejected request cannot leave a partial row — confirmed by a case that attempts three invalid writes and then asserts the table is empty.
Uniqueness and lookup (AC6-AC8). The UNIQUE constraint is (remote, org, repo, source_kind, source_number, target_kind, target_number, edge_type), and upsert_dependency_edge selects on exactly that tuple before choosing insert versus update, so re-observation refreshes one row. A dedicated index covers the target direction. The reverse-lookup case returns three waiters spanning two source kinds and two edge types while excluding an edge pointing at a different target. Scope isolation is asserted across repo and remote.
Audit trail (AC9). Transitions append to the existing events table with event_type dependency_edge_state_change and a message carrying prior and new state. An unchanged re-observation emits nothing, which keeps the audit table meaningful. work_item_id is NULL by design because an edge endpoint need never have been assigned; that column is nullable in the pre-existing schema, so no foreign key is violated.
Evidence safety (AC10). sanitize_evidence redacts keys matching a credential pattern and substitutes scheme-qualified URLs, recursing through mappings and sequences under depth and length bounds. The test reads the raw evidence column directly out of SQLite and asserts that neither the synthetic credential nor the scheme substring is present at rest, then asserts the same through the read path.
Allocator ingestion and non-interference (AC11, AC12, AC14). The write sits immediately after the existing resolve step and consumes its met/unmet/unavailable partitions directly, so no dependency is re-classified and unavailable evidence cannot be recorded as satisfied. The candidate fields feeding classify_skip are untouched. Non-interference is demonstrated behaviorally rather than argued: the same candidate set is allocated twice, once through a healthy store and once through a wrapper whose upsert always raises, and selection, skip set, and candidate count compare equal while the failure text surfaces in the reported reasons. A repeated allocation run leaves a single edge row.
Read-only exposure (AC13). gitea_list_dependency_edges gates on gitea.read before any other work, returns edges with read_only true, converts an invalid filter into a fail-closed result rather than an unfiltered set, and performs no write. It is added to docs/mcp-tool-inventory.md, which the issue #781 documentation drift guard compares against the registered tool list; that guard's suite passes at this head.
Findings
No blocking finding. Three non-blocking observations, recorded for the record and not conditions of this decision:
Scope discipline
Selection semantics, ranking, eligibility, and skip classification are unchanged, matching the issue's stated non-goals. Automatic blocking and resumption, non-issue edge creation, and defect auto-linking are correctly deferred to later slices. Umbrella issue #628 is not claimed, modified, or closed by this branch.
Controller Handoff
300e8acd13, mutation gate enforced0589ec80690589ec8069300e8acd130589ec80690589ec80690589ec8069, obtain explicit operator merge authorization, then merge PR #785 through the merger namespace0589ec8069against base master300e8acd13. Invoke the gitea-workflow skill first, then gitea_whoami, gitea_assess_master_parity, and gitea_resolve_task_capability immediately before each mutating call. Re-read PR 785 live and confirm the head SHA still equals the pinned SHA; a different head voids this verdict and requires a fresh review. Confirm the recorded verdict is an approval at that same head and that the approving identity differs from the authoring identity. Acquire the merger PR lease. Merge only with explicit operator merge authorization; absent that authorization, stop and report. After the merge event, hand off to the reconciler for label retirement and branch cleanup. Prohibited: merging a changed head, merging without operator authorization, claiming or closing umbrella issue 628, unrelated branch or worktree cleanup.0589ec8069, obtains explicit operator merge authorization, then merges PR #785[THREAD STATE LEDGER] PR #785 durable dependency edges — reviewer verdict recorded, merger acts next
What is true now:
300e8acd13, authored by jcwalker3 and reviewed by sysadminWhat changed:
What is blocked:
Who/what acts next:
0589ec8069, confirm explicit operator merge authorization, then merge PR #785 through the merger namespaceCanonical PR State
STATE:
review-complete-approve-recorded
WHO_IS_NEXT:
merger
NEXT_ACTION:
Confirm the live head still matches the pinned SHA and obtain explicit operator merge authorization, then merge PR #785 into master through the merger namespace.
NEXT_PROMPT:
WHAT_HAPPENED:
The reviewer session loaded the canonical review workflow, created a detached review worktree pinned to the PR head, read the complete diff across all six changed files, executed the full suite at the tested commit, executed the five affected files at the pre-merge base commit in a separate baseline worktree, and mapped every issue #784 acceptance criterion to the code and test that satisfies it. An APPROVE verdict is recorded at the reviewed head.
WHY:
The change adds the durable dependency-edge state umbrella #628 scope item 6 requires and that no existing component provided. It is correct against its stated acceptance criteria, fails closed at every input boundary, is proven not to alter allocator selection even when the new store raises on every write, and is confined to the files its issue names. The eleven failing tests reproduce identically at the pre-merge base commit, so they are not attributable to this branch.
ISSUE:
784
HEAD_SHA:
0589ec8069REVIEW_STATUS:
approve verdict recorded at head
0589ec8069by prgs-reviewer identity sysadminMERGE_READY:
yes at head
0589ec8069, contingent on explicit operator merge authorization and on the live head remaining unchangedBLOCKERS:
none
VALIDATION:
0589ec8069: 11 failed, 4126 passed, 6 skipped, 493 subtests passed300e8acd13in baseline worktree branches/review-pr-785-baseline: 11 failed, 254 passed across the same five files, identical node ids and identical assertion textLAST_UPDATED_BY:
sysadmin / prgs-reviewer / reviewer session / 2026-07-21
adopted_at: 2026-07-21T22:21:50Z
adopted_by_identity: sysadmin
adopted_by_profile: prgs-merger
adopted_from_session_id: 8563-20950baee7f0
adopted_from_profile: prgs-reviewer
adopted_from_reviewer_identity: sysadmin
adopted_from_comment_id: 13727
adoption_reason: merger-handoff-approved-head
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #785
issue: #784
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 8570-b92017159069
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-785-dependency-edges
phase: adopted
candidate_head:
0589ec8069target_branch: master
target_branch_sha:
300e8acd13last_activity: 2026-07-21T22:21:50Z
expires_at: 2026-07-21T22:31:50Z
blocker: none
Stale #332 review-decision lock cleanup (#594)
Status: APPLIED
sysadminprgs-merger2026-07-21T22:22:10.504248+00:00approveon PR feat(control-plane): persist allocator dependency edges as durable state (Closes #784) (#785)closed(merged=True)7ecf7bf2d666e179c2ecca27b60bb93f7092383f1prgs-reviewerManual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.