feat(afk): scheduler skips issues blocked by another open issue #136

Closed
opened 2026-07-11 13:18:45 +02:00 by dominik.polakovics · 1 comment

Problem

The AFK scheduler selects issues purely by `open + ready-for-agent, minus claim-branched, lowest number first`. The ## Blocked by section that the to-issues skill puts in every issue body is prose only — the scheduler happily schedules an issue whose blocker is still open, racing unmerged work.

Decision (from 2026-07-11 grill)

Promote the ## Blocked by body section to a machine-read contract: the scheduler must not schedule an issue while any issue it references there is still open.

Source of truth

The ## Blocked by markdown section in the issue body. Not Forgejo-native dependencies, not a label — bodies already arrive with ReadyIssues on all three tracker backends (forgejo, github, builtin), so this needs zero new tracker API surface.

Grammar

  • Case-insensitive Blocked by heading, any heading level; section text runs to the next heading.
  • Every #(\d+) in that section is a blocker. Refs elsewhere in the body ("relates to #12") never block.
  • No refs found (including "None - can start immediately") → unblocked.
  • Multiple refs → blocked until all resolve.
  • Cross-repo (owner/repo#N) and full-URL refs: ignored by the filter (unevaluable per-repo) but logged, so a real cross-repo dependency doesn't silently schedule.
  • Style should mirror the tolerant grammar of closesDirectiveRe in internal/tracker/closes.go.

Semantics

  • A ref blocks iff that issue exists and is currently open. Closed unblocks regardless of how it closed (merged, wontfix, no PR).
  • Dangling ref (nonexistent/deleted issue) → unblocked. Fail toward progress on data ambiguity.
  • Blocker open but already claimed/in-flight → still blocks (unmerged work is not done work).
  • Cycles (A↔B) simply never schedule — triage data bug, no cycle detection in the scheduler.

Placement

  • Pure functions in internal/afk beside the existing decision functions (decide.go style): ParseBlockedBy(body) []int and a filter taking the set of open issue numbers.
  • Applied inside FilterClaimable (internal/afk/launch.go:252) — the choke point shared by auto scheduling, StartManualAFK, and ClaimableIssuesFor — so scheduler, manual starts, and the UI's claimable queue all agree. Manual starts skip blocked issues identically (there is no targeted-issue start today).
  • Open-set via the existing Tracker.Issues(ctx, tracker.StateOpen), fetched only when at least one ready issue actually contains blocker refs — zero added forge calls for repos not using the convention.

Edge behavior

  • Skipped-blocked issues produce a structured log line naming the blockers (e.g. afk: repo X skipped blocked issues: #87 (blocked by #74)). No UI change in this slice (a "blocked" badge is a candidate follow-up issue).
  • All ready issues blocked ≡ empty ready queue: no launch, no consecutive-failure increment, no pause.
  • Issues(StateOpen) fetch failure → skip that repo's tick, per the existing Tracker contract (fail-closed on infrastructure errors, fail-open only on data ambiguity).

Docs contract

  1. Short ADR in docs/adr/: source of truth, grammar, semantics, placement (precedent: claim-is-the-branch ADR).
  2. assets/skills/to-issues/SKILL.md: note the ## Blocked by section is machine-read by the AFK scheduler; blockers must be written as #N — prose-only blockers do not block scheduling.
  3. Triage docs note: ready-for-agent + live ## Blocked by refs is a valid combination — triage should not hold the label back waiting on blockers; the scheduler handles ordering.

Acceptance criteria

  • Table tests for ParseBlockedBy and the filter in decide_test.go style: section-scoped extraction, none-case, multiple refs, refs outside the section ignored, heading-level/case tolerance.
  • Engine-level test: a lower-numbered ready issue blocked by an open issue is skipped and the next unblocked ready issue is claimed; after the blocker closes, the previously blocked issue is claimed on a subsequent sweep.
  • Test: all-blocked queue behaves exactly like an empty queue (no launch, no failure increment).
  • No extra Issues(StateOpen) call when no ready issue contains blocker refs.
  • ADR + both skill/doc updates included.

Blocked by

None - can start immediately

## Problem The AFK scheduler selects issues purely by \`open + ready-for-agent, minus claim-branched, lowest number first\`. The `## Blocked by` section that the to-issues skill puts in every issue body is prose only — the scheduler happily schedules an issue whose blocker is still open, racing unmerged work. ## Decision (from 2026-07-11 grill) Promote the `## Blocked by` body section to a machine-read contract: the scheduler must not schedule an issue while any issue it references there is still open. ### Source of truth The `## Blocked by` markdown section in the issue body. Not Forgejo-native dependencies, not a label — bodies already arrive with `ReadyIssues` on all three tracker backends (forgejo, github, builtin), so this needs zero new tracker API surface. ### Grammar - Case-insensitive `Blocked by` heading, any heading level; section text runs to the next heading. - Every `#(\d+)` in that section is a blocker. Refs elsewhere in the body ("relates to #12") never block. - No refs found (including "None - can start immediately") → unblocked. - Multiple refs → blocked until all resolve. - Cross-repo (`owner/repo#N`) and full-URL refs: ignored by the filter (unevaluable per-repo) but logged, so a real cross-repo dependency doesn't silently schedule. - Style should mirror the tolerant grammar of `closesDirectiveRe` in `internal/tracker/closes.go`. ### Semantics - A ref blocks iff that issue exists and is currently **open**. Closed unblocks regardless of how it closed (merged, wontfix, no PR). - Dangling ref (nonexistent/deleted issue) → unblocked. Fail toward progress on data ambiguity. - Blocker open but already claimed/in-flight → **still blocks** (unmerged work is not done work). - Cycles (A↔B) simply never schedule — triage data bug, no cycle detection in the scheduler. ### Placement - Pure functions in `internal/afk` beside the existing decision functions (`decide.go` style): `ParseBlockedBy(body) []int` and a filter taking the set of open issue numbers. - Applied inside `FilterClaimable` (`internal/afk/launch.go:252`) — the choke point shared by auto scheduling, `StartManualAFK`, and `ClaimableIssuesFor` — so scheduler, manual starts, and the UI's claimable queue all agree. Manual starts skip blocked issues identically (there is no targeted-issue start today). - Open-set via the existing `Tracker.Issues(ctx, tracker.StateOpen)`, fetched **only when at least one ready issue actually contains blocker refs** — zero added forge calls for repos not using the convention. ### Edge behavior - Skipped-blocked issues produce a structured log line naming the blockers (e.g. `afk: repo X skipped blocked issues: #87 (blocked by #74)`). No UI change in this slice (a "blocked" badge is a candidate follow-up issue). - All ready issues blocked ≡ empty ready queue: no launch, no consecutive-failure increment, no pause. - `Issues(StateOpen)` fetch failure → skip that repo's tick, per the existing Tracker contract (fail-closed on infrastructure errors, fail-open only on data ambiguity). ### Docs contract 1. Short ADR in `docs/adr/`: source of truth, grammar, semantics, placement (precedent: claim-is-the-branch ADR). 2. `assets/skills/to-issues/SKILL.md`: note the `## Blocked by` section is machine-read by the AFK scheduler; blockers must be written as `#N` — prose-only blockers do not block scheduling. 3. Triage docs note: `ready-for-agent` + live `## Blocked by` refs is a valid combination — triage should not hold the label back waiting on blockers; the scheduler handles ordering. ## Acceptance criteria - Table tests for `ParseBlockedBy` and the filter in `decide_test.go` style: section-scoped extraction, none-case, multiple refs, refs outside the section ignored, heading-level/case tolerance. - Engine-level test: a lower-numbered ready issue blocked by an open issue is skipped and the next unblocked ready issue is claimed; after the blocker closes, the previously blocked issue is claimed on a subsequent sweep. - Test: all-blocked queue behaves exactly like an empty queue (no launch, no failure increment). - No extra `Issues(StateOpen)` call when no ready issue contains blocker refs. - ADR + both skill/doc updates included. ## Blocked by None - can start immediately
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PR #138 → VERDICT: PASS

  • CI: ci / native success (run 143, 5m36s) — relied on as the verification signal; build/tests/lint/vet not re-run.
  • Conflicts: none — merges cleanly into origin/main (merge-tree clean).
  • Convention: title feat(afk): scheduler skips issues blocked by another open issue is Conventional Commits; head afk/136 carries a working Closes #136.
  • Diff vs intent: matches this issue's grill decisions point-for-point — section-scoped ## Blocked by grammar mirroring closesDirectiveRe; open-blocks / closed-&-dangling-unblock / claimed-still-blocks semantics; single FilterClaimable choke point shared by launch, scheduler pre-tick, ClaimableIssuesFor, and GET /repos/{id}/ready; lazy Issues(StateOpen) fetch armed only when a claimable issue carries a local #N ref; fail-closed (*TrackerError) on infra, fail-open on data ambiguity; structured afk: skipped blocked issues … log line.
  • Tests: table tests (32 subtests) + engine-level skip/claim-on-later-sweep, all-blocked≡empty, no-fetch-without-refs, fetch-failure-fails-closed on both auto and manual paths.

No blockers. Awaiting free-text merge go-ahead from the operator.

> *This was generated by AI while landing a PR.* **Landing audit — PR #138 → VERDICT: PASS** - **CI**: `ci / native` **success** (run 143, 5m36s) — relied on as the verification signal; build/tests/lint/vet not re-run. - **Conflicts**: none — merges cleanly into `origin/main` (merge-tree clean). - **Convention**: title `feat(afk): scheduler skips issues blocked by another open issue` is Conventional Commits; head `afk/136` carries a working `Closes #136`. - **Diff vs intent**: matches this issue's grill decisions point-for-point — section-scoped `## Blocked by` grammar mirroring `closesDirectiveRe`; open-blocks / closed-&-dangling-unblock / claimed-still-blocks semantics; single `FilterClaimable` choke point shared by launch, scheduler pre-tick, `ClaimableIssuesFor`, and `GET /repos/{id}/ready`; lazy `Issues(StateOpen)` fetch armed only when a claimable issue carries a local `#N` ref; fail-closed (`*TrackerError`) on infra, fail-open on data ambiguity; structured `afk: skipped blocked issues …` log line. - **Tests**: table tests (32 subtests) + engine-level skip/claim-on-later-sweep, all-blocked≡empty, no-fetch-without-refs, fetch-failure-fails-closed on both auto and manual paths. No blockers. Awaiting free-text merge go-ahead from the operator.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#136
No description provided.