Make work allocation complete and dependency-aware #758

Closed
opened 2026-07-19 01:02:51 -05:00 by jcwalker3 · 1 comment
Owner

Summary

The author allocator can select the wrong or ineligible issue because it truncates the candidate set before ranking and does not parse canonical Depends: declarations. When the selected issue fails the preview-before-claim check, allocation stops instead of continuing to the next eligible candidate.

This makes a supervised hourly author loop repeatedly terminate without producing work even when other eligible issues exist.

Discovered by the first supervised /gitea-author-loop iteration. Verified against master bde5c5fb20fbf6aec9cd6b802341c43078921d13.

Observed behavior

A result-size parameter changes the winning candidate

Two read-only dry runs of gitea_allocate_next_work(role=author, apply=false) against Scaled-Tech-Consulting/Gitea-Tools, differing only in limit:

Run limit candidate_count selected
A default (50) 50 issue #642
B 300 73 issue #605

Both returned outcome: "preview", priority: 20, inventory_source: "gitea_live", an empty skipped list, and the identical reason_selected of "highest-priority candidate for role 'author'".

limit is shaped and documented as a result/diagnostic bound. In practice it changes which candidate wins, not merely how many are reported.

A dependency-blocked issue was emitted as eligible

Issue #642 declares its dependencies in the repository's canonical form:

* Parent: #631 · Depends: #633, #634 · Related: #630, #434, #591, #584

Both #633 and #634 are open. Despite that, #642 was emitted without dependency_unmet=true, did not appear in skipped, and was selected as the winner in run A.

The supervised author loop correctly refused to claim #642 at the preview-before-claim eligibility check. It then had no sanctioned way to request the next eligible candidate during the same iteration, so the iteration ended having performed no work.

Root cause

Two independent defects, plus one consequence of their interaction.

Defect 1 — candidate truncation occurs before ranking

gitea_mcp_server.py:17800-17802 fetches the complete open-issue inventory through the paginating helper:

issues = api_get_all(
    f"{repo_api_url(h, o, r)}/issues?state=open&type=issues", auth
) or []

gitea_mcp_server.py:17814 then discards most of it before any candidate is constructed:

for issue in issues[: max(1, int(limit))]:

limit defaults to 50 in the tool signature at gitea_mcp_server.py:18091. Ranking happens later, in allocator_service.sort_candidates (allocator_service.py:244-249).

This is therefore not a pagination shortfall. The full inventory is already in memory and is deliberately sliced ahead of ranking, so candidates 51..73 never become WorkCandidate objects and cannot win. The equivalent PR-side truncation is at gitea_mcp_server.py:17742.

Defect 2 — dependency metadata is not populated for Depends: syntax

gitea_mcp_server.py:17837-17846 recognizes only two lowercase body phrases:

lower_body = body.lower()
if "blocked on #" in lower_body or "downstream of #" in lower_body:
    dep_unmet = "blocked on #" in lower_body

The canonical Depends: #633, #634 form matches neither phrase, so dep_unmet stays False and is passed as dependency_unmet=dep_unmet at gitea_mcp_server.py:17857.

No live lookup of the referenced issues' states occurs anywhere in this path. Dependency state is inferred purely from substring presence in body text and is never verified against the actual issues.

Consequence — allocation cannot advance past an ineligible winner

Ranking is effectively flat across the ready queue. gitea_mcp_server.py:17855 assigns:

priority=20 if "status:ready" in labels else 1,

Every status:ready issue therefore ties at 20, and allocator_service.py:244-249 breaks ties deterministically by (-priority, kind != "pr", number) — lowest issue number wins among ties. Ordering within the ready queue is thus decided entirely by which issues survived the Defect 1 slice.

End to end: the allocator selects a candidate that Defect 2 failed to mark blocked; the preview-before-claim check correctly refuses it; and the allocator exposes no sanctioned way to fall through to the next eligible candidate within the same iteration. The queue terminates rather than advancing.

Impact

  • Scheduled author iterations can repeatedly make no progress while eligible work exists.
  • Blocked work can appear claimable, inviting a claim that violates declared dependency order.
  • Candidate ordering changes based on an output-size parameter, so two callers asking the same question receive different answers.
  • Operators may be tempted to edit labels manually or bypass the allocator to force progress.
  • Dry-run selection may not represent the complete queue, so preview is not a trustworthy basis for a decision.

Required implementation investigation

  • gitea_mcp_server.py — allocator candidate-loading helper (issue and PR paths), gitea_allocate_next_work signature and limit semantics.
  • allocator_service.pyWorkCandidate, sort_candidates, allocate_next_work, and the eligibility/preview path.
  • api_get_all pagination conventions, to confirm inventory completeness before ranking.
  • The preview-before-claim eligibility path, to determine where next-candidate iteration belongs.

Preferred direction: separate inventory completeness from result presentation. Rank the full candidate set, then apply any display bound to the reported output only. Parse dependency declarations into structured references and resolve their states from live issue data rather than from body substrings.

Acceptance criteria

AC1. Allocation ranks the complete relevant candidate inventory.

AC2. A diagnostic or response limit cannot alter the selected candidate.

AC3. Pagination is completed before ranking, or an incomplete inventory fails closed explicitly.

AC4. Canonical Depends: #N, #N declarations are parsed into structured dependency references without issue-number special cases.

AC5. Dependency state is verified from live issue evidence.

AC6. Unresolved dependencies make a candidate ineligible.

AC7. Unavailable dependency evidence fails closed for that candidate.

AC8. When the first preview candidate is blocked, allocation continues deterministically to the next eligible candidate during the same iteration.

AC9. Dry-run and apply mode use identical inventory, dependency, priority, and selection logic.

AC10. Priority and tie-breaking behavior is explicit and documented. A flat status:ready score must not conceal accidental ordering caused by truncation.

AC11. Tests use more than 50 candidates and prove selection invariance across result limits.

AC12. Tests cover multiple open dependencies, closed dependencies, unavailable dependency evidence, and next-candidate selection.

AC13. No-mutation dry-run behavior and lock-before-edit safeguards are preserved.

AC14. No production behavior is special-cased for #642, #633, #634, or #605.

Validation expectations

  • Focused unit tests for the dependency parser: canonical Depends: forms, multiple references, mixed open/closed states, and malformed input.
  • Unit tests for candidate-inventory assembly proving no pre-ranking truncation.
  • An MCP-level allocator regression exercising gitea_allocate_next_work end to end.
  • Dry-run versus apply parity tests proving identical inventory, dependency evaluation, ranking, and selection.
  • Candidate inventories above the former default limit of 50, asserting limit-invariant selection.
  • The relevant full-suite baseline comparison, reported against known pre-existing master failures rather than as an absolute pass count.

Linkage and ownership boundary

  • #600, #613 — closed; the allocator feature and its control-plane DB substrate. These are the components being corrected; do not reopen them.
  • #628 — open; integration umbrella for dependency-aware concurrent orchestration. It proposes a new durable dependency table and explicitly disclaims re-owning sibling components. This issue owns the present-day loader and selection defects; do not fold it into #628 or re-own #628's scope here.
  • #627 — closed; paginated label resolution in gitea_set_issue_labels. Similar-sounding, but a different tool and a genuine pagination shortfall, whereas Defect 1 here is post-fetch truncation of an already-complete list.
  • #605 — open; read-only queue dashboard that defers assignment to #600. Not the selection logic.
  • #642, #633, #634, #605 — evidence only. Do not modify these issues, their labels, or their scope as part of this work.

Canonical issue state

STATE: ready-for-author
WHO_IS_NEXT: author
NEXT_ACTION: Lock this issue in a separately bounded author task, then implement AC1-AC14
NEXT_PROMPT: Author the allocator completeness and dependency-awareness fix; PR; stop
## Summary The author allocator can select the wrong or ineligible issue because it truncates the candidate set before ranking and does not parse canonical `Depends:` declarations. When the selected issue fails the preview-before-claim check, allocation stops instead of continuing to the next eligible candidate. This makes a supervised hourly author loop repeatedly terminate without producing work even when other eligible issues exist. Discovered by the first supervised `/gitea-author-loop` iteration. Verified against master `bde5c5fb20fbf6aec9cd6b802341c43078921d13`. ## Observed behavior ### A result-size parameter changes the winning candidate Two read-only dry runs of `gitea_allocate_next_work(role=author, apply=false)` against `Scaled-Tech-Consulting/Gitea-Tools`, differing only in `limit`: | Run | `limit` | `candidate_count` | selected | |-----|---------|-------------------|----------| | A | default (50) | 50 | issue **#642** | | B | 300 | 73 | issue **#605** | Both returned `outcome: "preview"`, `priority: 20`, `inventory_source: "gitea_live"`, an empty `skipped` list, and the identical `reason_selected` of "highest-priority candidate for role 'author'". `limit` is shaped and documented as a result/diagnostic bound. In practice it changes **which candidate wins**, not merely how many are reported. ### A dependency-blocked issue was emitted as eligible Issue #642 declares its dependencies in the repository's canonical form: ```text * Parent: #631 · Depends: #633, #634 · Related: #630, #434, #591, #584 ``` Both #633 and #634 are open. Despite that, #642 was emitted without `dependency_unmet=true`, did not appear in `skipped`, and was selected as the winner in run A. The supervised author loop correctly refused to claim #642 at the preview-before-claim eligibility check. It then had no sanctioned way to request the next eligible candidate during the same iteration, so the iteration ended having performed no work. ## Root cause Two independent defects, plus one consequence of their interaction. ### Defect 1 — candidate truncation occurs before ranking `gitea_mcp_server.py:17800-17802` fetches the **complete** open-issue inventory through the paginating helper: ```python issues = api_get_all( f"{repo_api_url(h, o, r)}/issues?state=open&type=issues", auth ) or [] ``` `gitea_mcp_server.py:17814` then discards most of it before any candidate is constructed: ```python for issue in issues[: max(1, int(limit))]: ``` `limit` defaults to `50` in the tool signature at `gitea_mcp_server.py:18091`. Ranking happens later, in `allocator_service.sort_candidates` (`allocator_service.py:244-249`). This is therefore **not** a pagination shortfall. The full inventory is already in memory and is deliberately sliced ahead of ranking, so candidates 51..73 never become `WorkCandidate` objects and cannot win. The equivalent PR-side truncation is at `gitea_mcp_server.py:17742`. ### Defect 2 — dependency metadata is not populated for `Depends:` syntax `gitea_mcp_server.py:17837-17846` recognizes only two lowercase body phrases: ```python lower_body = body.lower() if "blocked on #" in lower_body or "downstream of #" in lower_body: dep_unmet = "blocked on #" in lower_body ``` The canonical `Depends: #633, #634` form matches neither phrase, so `dep_unmet` stays `False` and is passed as `dependency_unmet=dep_unmet` at `gitea_mcp_server.py:17857`. No live lookup of the referenced issues' states occurs anywhere in this path. Dependency state is inferred purely from substring presence in body text and is never verified against the actual issues. ### Consequence — allocation cannot advance past an ineligible winner Ranking is effectively flat across the ready queue. `gitea_mcp_server.py:17855` assigns: ```python priority=20 if "status:ready" in labels else 1, ``` Every `status:ready` issue therefore ties at 20, and `allocator_service.py:244-249` breaks ties deterministically by `(-priority, kind != "pr", number)` — lowest issue number wins among ties. Ordering within the ready queue is thus decided entirely by which issues survived the Defect 1 slice. End to end: the allocator selects a candidate that Defect 2 failed to mark blocked; the preview-before-claim check correctly refuses it; and the allocator exposes no sanctioned way to fall through to the next eligible candidate within the same iteration. The queue terminates rather than advancing. ## Impact * Scheduled author iterations can repeatedly make no progress while eligible work exists. * Blocked work can appear claimable, inviting a claim that violates declared dependency order. * Candidate ordering changes based on an output-size parameter, so two callers asking the same question receive different answers. * Operators may be tempted to edit labels manually or bypass the allocator to force progress. * Dry-run selection may not represent the complete queue, so preview is not a trustworthy basis for a decision. ## Required implementation investigation * `gitea_mcp_server.py` — allocator candidate-loading helper (issue and PR paths), `gitea_allocate_next_work` signature and `limit` semantics. * `allocator_service.py` — `WorkCandidate`, `sort_candidates`, `allocate_next_work`, and the eligibility/preview path. * `api_get_all` pagination conventions, to confirm inventory completeness before ranking. * The preview-before-claim eligibility path, to determine where next-candidate iteration belongs. Preferred direction: separate **inventory completeness** from **result presentation**. Rank the full candidate set, then apply any display bound to the reported output only. Parse dependency declarations into structured references and resolve their states from live issue data rather than from body substrings. ## Acceptance criteria **AC1.** Allocation ranks the complete relevant candidate inventory. **AC2.** A diagnostic or response limit cannot alter the selected candidate. **AC3.** Pagination is completed before ranking, or an incomplete inventory fails closed explicitly. **AC4.** Canonical `Depends: #N, #N` declarations are parsed into structured dependency references without issue-number special cases. **AC5.** Dependency state is verified from live issue evidence. **AC6.** Unresolved dependencies make a candidate ineligible. **AC7.** Unavailable dependency evidence fails closed for that candidate. **AC8.** When the first preview candidate is blocked, allocation continues deterministically to the next eligible candidate during the same iteration. **AC9.** Dry-run and apply mode use identical inventory, dependency, priority, and selection logic. **AC10.** Priority and tie-breaking behavior is explicit and documented. A flat `status:ready` score must not conceal accidental ordering caused by truncation. **AC11.** Tests use more than 50 candidates and prove selection invariance across result limits. **AC12.** Tests cover multiple open dependencies, closed dependencies, unavailable dependency evidence, and next-candidate selection. **AC13.** No-mutation dry-run behavior and lock-before-edit safeguards are preserved. **AC14.** No production behavior is special-cased for #642, #633, #634, or #605. ## Validation expectations * Focused unit tests for the dependency parser: canonical `Depends:` forms, multiple references, mixed open/closed states, and malformed input. * Unit tests for candidate-inventory assembly proving no pre-ranking truncation. * An MCP-level allocator regression exercising `gitea_allocate_next_work` end to end. * Dry-run versus apply parity tests proving identical inventory, dependency evaluation, ranking, and selection. * Candidate inventories above the former default limit of 50, asserting limit-invariant selection. * The relevant full-suite baseline comparison, reported against known pre-existing master failures rather than as an absolute pass count. ## Linkage and ownership boundary * **#600**, **#613** — closed; the allocator feature and its control-plane DB substrate. These are the components being corrected; do not reopen them. * **#628** — open; integration umbrella for dependency-aware concurrent orchestration. It proposes a new durable dependency table and explicitly disclaims re-owning sibling components. **This issue owns the present-day loader and selection defects**; do not fold it into #628 or re-own #628's scope here. * **#627** — closed; paginated label resolution in `gitea_set_issue_labels`. Similar-sounding, but a different tool and a genuine pagination shortfall, whereas Defect 1 here is post-fetch truncation of an already-complete list. * **#605** — open; read-only queue dashboard that defers assignment to #600. Not the selection logic. * **#642**, **#633**, **#634**, **#605** — evidence only. Do not modify these issues, their labels, or their scope as part of this work. ## Canonical issue state ```text STATE: ready-for-author WHO_IS_NEXT: author NEXT_ACTION: Lock this issue in a separately bounded author task, then implement AC1-AC14 NEXT_PROMPT: Author the allocator completeness and dependency-awareness fix; PR; stop ```
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-19 13:13:47 -05:00
Author
Owner

CTH: Author Handoff

[THREAD STATE LEDGER] Issue #758 / PR #761 — author implementation pushed, awaiting independent review

What is true now:

  • Issue state: open, status:pr-open
  • Owning PR: #761 (open)
  • Current head SHA: ad59053cd7
  • Base: master at 854818e65a
  • Server-side decision state: no formal review verdict exists for PR #761
  • Local verdict/state: not applicable — this was an author session with no review authority
  • Latest known validation: full suite 3672 passed, 2 failed, 6 skipped, 431 subtests passed; the 2 failures reproduce identically at unmodified master 854818e

What changed:

  • Branch fix/issue-758-allocator-dependency-aware created from master 854818e and pushed
  • Commit ad59053 landed on that branch: allocator ranks the complete candidate inventory, and canonical Depends declarations resolve against live issue state
  • New module allocator_dependencies.py; 35 new tests across tests/test_allocator_dependencies.py and tests/test_allocator_inventory_mcp.py
  • PR #761 opened against master; issue label transitioned to status:pr-open

What is blocked:

  • Blocker classification: no blocker

Who/what acts next:

  • Next actor: reviewer
  • Required action: independent formal review of PR #761 at pinned head ad59053cd7 against AC1-AC14, using the gitea-reviewer namespace and prgs-reviewer profile
  • Do not do: do not re-run the author phase, do not re-implement, do not create a second branch or PR for this issue, do not self-review from an author-bound session
  • Resume from: PR #761 at head ad59053cd7

Canonical Issue State

STATE:
PR-open

WHO_IS_NEXT:
reviewer

NEXT_ACTION:
Independently review PR #761 at head ad59053cd7 against acceptance criteria AC1 through AC14 and post the formal verdict.

NEXT_PROMPT:
Review PR #761 in Scaled-Tech-Consulting/Gitea-Tools using the gitea-reviewer namespace and the prgs-reviewer profile. Pin head ad59053cd7 before verifying anything. Check AC1-AC14 of issue #758, in particular that candidate inventory is ranked without pre-ranking truncation, that a result limit cannot change the selected candidate, that canonical Depends declarations resolve from live issue state, and that unavailable dependency evidence fails closed. Confirm the two full-suite failures are pre-existing at master 854818e and not caused by this branch. Submit the formal verdict and stop.

WHAT_HAPPENED:
A scheduled controller cycle selected #758 for the author phase. The canonical allocator preview was run first and returned issue #644, whose body declares Depends on #633, #636 and #641 — all three still open — which reproduced this issue's Defect 2 live, with candidate_count 50 confirming Defect 1. #758 was locked, implemented on a dedicated branch, checked against the full test suite, pushed, and opened as PR #761.

WHY:
#758 declares no dependencies of its own, carries type:bug with an author-eligible queue label, and owns the loader and selection defects that were preventing correct queue selection. Taking the allocator's own preview instead would have violated the declared dependency order that this issue exists to enforce.

RELATED_DISCUSSION:
none

RELATED_PRS:

BRANCH:
fix/issue-758-allocator-dependency-aware

HEAD_SHA:
ad59053cd7

VALIDATION:
Full suite via /opt/homebrew/bin/python3 -m pytest tests/: 3672 passed, 2 failed, 6 skipped, 1 warning, 431 subtests passed. Pre-existing failures test_issue_702_review_findings_f1_f6.py::TestF1RecoveryBeforeTerminalProbe::test_removed_worktree_recovers_before_probe and test_reconciler_supersession_close.py::TestReconcilerSupersessionMcpTool::test_tool_posts_comment_and_closes_superseded_pr_issue were reproduced identically in a detached worktree at unmodified master 854818e before commit; neither touches allocator code. Targeted allocator suites: 65 passed.

BLOCKERS:
none

LAST_UPDATED_BY:
jcwalker3 / prgs-author / 2026-07-19

## CTH: Author Handoff [THREAD STATE LEDGER] Issue #758 / PR #761 — author implementation pushed, awaiting independent review What is true now: - Issue state: open, status:pr-open - Owning PR: #761 (open) - Current head SHA: ad59053cd7a2e7ecec60901666160a73eaa36efa - Base: master at 854818e65a2741ba0740773fd9593d5e1f3d5751 - Server-side decision state: no formal review verdict exists for PR #761 - Local verdict/state: not applicable — this was an author session with no review authority - Latest known validation: full suite 3672 passed, 2 failed, 6 skipped, 431 subtests passed; the 2 failures reproduce identically at unmodified master 854818e What changed: - Branch fix/issue-758-allocator-dependency-aware created from master 854818e and pushed - Commit ad59053 landed on that branch: allocator ranks the complete candidate inventory, and canonical Depends declarations resolve against live issue state - New module allocator_dependencies.py; 35 new tests across tests/test_allocator_dependencies.py and tests/test_allocator_inventory_mcp.py - PR #761 opened against master; issue label transitioned to status:pr-open What is blocked: - Blocker classification: no blocker Who/what acts next: - Next actor: reviewer - Required action: independent formal review of PR #761 at pinned head ad59053cd7a2e7ecec60901666160a73eaa36efa against AC1-AC14, using the gitea-reviewer namespace and prgs-reviewer profile - Do not do: do not re-run the author phase, do not re-implement, do not create a second branch or PR for this issue, do not self-review from an author-bound session - Resume from: PR #761 at head ad59053cd7a2e7ecec60901666160a73eaa36efa ## Canonical Issue State STATE: PR-open WHO_IS_NEXT: reviewer NEXT_ACTION: Independently review PR #761 at head ad59053cd7a2e7ecec60901666160a73eaa36efa against acceptance criteria AC1 through AC14 and post the formal verdict. NEXT_PROMPT: Review PR #761 in Scaled-Tech-Consulting/Gitea-Tools using the gitea-reviewer namespace and the prgs-reviewer profile. Pin head ad59053cd7a2e7ecec60901666160a73eaa36efa before verifying anything. Check AC1-AC14 of issue #758, in particular that candidate inventory is ranked without pre-ranking truncation, that a result limit cannot change the selected candidate, that canonical Depends declarations resolve from live issue state, and that unavailable dependency evidence fails closed. Confirm the two full-suite failures are pre-existing at master 854818e and not caused by this branch. Submit the formal verdict and stop. WHAT_HAPPENED: A scheduled controller cycle selected #758 for the author phase. The canonical allocator preview was run first and returned issue #644, whose body declares Depends on #633, #636 and #641 — all three still open — which reproduced this issue's Defect 2 live, with candidate_count 50 confirming Defect 1. #758 was locked, implemented on a dedicated branch, checked against the full test suite, pushed, and opened as PR #761. WHY: #758 declares no dependencies of its own, carries type:bug with an author-eligible queue label, and owns the loader and selection defects that were preventing correct queue selection. Taking the allocator's own preview instead would have violated the declared dependency order that this issue exists to enforce. RELATED_DISCUSSION: none RELATED_PRS: - #761 BRANCH: fix/issue-758-allocator-dependency-aware HEAD_SHA: ad59053cd7a2e7ecec60901666160a73eaa36efa VALIDATION: Full suite via /opt/homebrew/bin/python3 -m pytest tests/: 3672 passed, 2 failed, 6 skipped, 1 warning, 431 subtests passed. Pre-existing failures test_issue_702_review_findings_f1_f6.py::TestF1RecoveryBeforeTerminalProbe::test_removed_worktree_recovers_before_probe and test_reconciler_supersession_close.py::TestReconcilerSupersessionMcpTool::test_tool_posts_comment_and_closes_superseded_pr_issue were reproduced identically in a detached worktree at unmodified master 854818e before commit; neither touches allocator code. Targeted allocator suites: 65 passed. BLOCKERS: none LAST_UPDATED_BY: jcwalker3 / prgs-author / 2026-07-19
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#758