From d4ce41592b20ff43c5532638a04ac7c0ba21dba0 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:54:21 -0400 Subject: [PATCH 1/4] fix(work-items): exclude open-linked-PR issues from work frontier (#463) /work-items:work selection had no rule excluding an issue that already has an open linked PR from the pickable frontier, so an issue kept `status: ready` for its entire open-PR window and a picker had to hand-cross-check `gh pr list` to avoid starting a duplicate branch. Add a selection-time frontier filter (tiers 2-3) that drops a candidate with an open PR closing it, routed through a new GitHub adapter "Open linked PRs" mechanic (closing-keyword linkage authoritative; `Refs #N` opt-out does not exclude), failing open when the bound provider exposes no PR host. Retires the interim in-flight heuristic from the execute-step staleness pre-check. Durable seam-level in-review state remains deferred to the tracker-seam layer (#416/#498). Co-Authored-By: Claude Sonnet 5 --- plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 14 ++++++++++++ plugins/work-items/skills/work/SKILL.md | 3 ++- .../adapters/github/README.md | 22 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 23d58cd90..4f8f5ea23 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.13.0", + "version": "0.13.1", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github and local-markdown adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, and raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states). The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 66e3479ec..a3a78bdb0 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.13.1] + +### Fixed + +- **`status: ready` issues with an open linked PR are no longer pickable (`#463`).** `/work-items:work` + selection now excludes a frontier candidate (tiers 2–3) that already has an open PR targeting it for + closure, closing the re-pick risk where an issue kept `status: ready` for its entire open-PR window and + a picker had to hand-cross-check `gh pr list` to avoid starting a duplicate branch. The check routes + through a new GitHub adapter *Open linked PRs* mechanic (closing-keyword linkage — the same `Closes #N` + signal `pr-issue-linkage` enforces — is authoritative; an intentional `Refs #N` opt-out does not + exclude), and fails open when the bound provider exposes no PR host (offline `local-markdown`). This + retires the interim in-flight heuristic that lived in the execute-step staleness pre-check. The durable + seam-level in-review state is deferred to the tracker-seam layer (`#416`/`#498`), not built here. + ## [0.13.0] Absorb the v4 loop-prompt execution rules into `/work-items:work` so the execute step owns them diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index abfc1d150..d5930068a 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -94,6 +94,8 @@ For each tier, emit the corresponding query: `--autonomous` additionally excludes items carrying the human-gated role label (`needs-human` by default — [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Canonical roles"). Tier 2 keeps only `area: guardrails` items that do not carry the resolved recurring-maintenance label; tier 3 keeps the rest of that non-recurring set. The normalized frontier model omits `createdAt`, so apply tier 3's **oldest-first** ordering by sorting the candidates on `createdAt` from the adapter "List items" projection (over the frontier numbers) before picking the top one — pass an explicit `--limit` covering the whole frontier on that projection so the default truncation can't hide an older candidate outside the first page and defeat the oldest-first pick (page per the adapter "List items" note if the frontier exceeds the max page size). Provider search syntax never leaves the adapter — the label filter runs over the labels `list-frontier` already returns. +**Exclude in-flight frontier candidates (open linked PR).** A frontier candidate (tiers 2–3) that already has an open PR targeting it for closure is work already in flight — drop it from the pickable set so it is not re-picked. For each surviving frontier number, query the bound adapter's *open linked PRs* operation (GitHub: [`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md) "Open linked PRs") and exclude the number when it reports an open PR closing that item. The **closing-keyword linkage** is the authoritative signal — the same `Closes #N` / native-closing-keyword linkage `pr-issue-linkage` enforces (owned by `/source-control:pull-request`), so an intentional `Refs #N` opt-out does not exclude its issue. Provider search syntax never leaves the adapter — this filter reads only the open-closing-PR boolean the adapter returns per number. **Fail open when the bound provider exposes no PR host:** the offline `local-markdown` binding is never a coordination surface and touches no network tool ([`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md) "local-markdown adapter"), so when no PR-host operation is available this filter has nothing to query — keep the candidate rather than blocking or reaching for a network tool. + Tiers flagged `last-resort: true` are skipped if any prior tier yielded a candidate. ### Step 2: Cross-reference with open items @@ -122,7 +124,6 @@ Before claiming, verify the item is still actionable: - If it references a file to modify: check the file exists and the item is still relevant. - If it references a test to add: check whether similar tests already exist. - If stale (work already done): close it with a comment (adapter: "Close item") and advance to the next candidate. -- **Already-in-flight (interim, retire on `#463`):** skip a frontier item that already has an open PR — advance to the next candidate rather than starting a duplicate branch. Detect the open PR through the PR host: match an open PR whose head branch follows this item's `/-` branch pattern (the *Suggest branch name* convention below) or whose body carries a GitHub issue-closing keyword for this item — any of `close`/`closes`/`closed`, `fix`/`fixes`/`fixed`, `resolve`/`resolves`/`resolved` (case-insensitive, optional colon) followed by `#` with exact-number boundaries, the same nine forms `/source-control:pull-request` recognizes, not only the `Closes #` the branch parse injects (a PR opened from an orphan or noncanonical branch can link via any of them). **Fail open when there is no PR host:** the offline `local-markdown` binding is never a coordination surface and touches no network tool, so when no PR host is configured or available this check has nothing to query — skip it and proceed to claim rather than blocking or reaching for a network tool. This is an interim heuristic; retire it once the durable in-progress marker (`#463`) lands and the frontier itself excludes in-flight items. ### Step 5: Claim and execute diff --git a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md index 285ad82a6..2ca2d463d 100644 --- a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md +++ b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md @@ -155,6 +155,28 @@ Match GitHub's issue-closing keyword set (`close`/`closes`/`closed`/`fix`/`fixes `resolve`/`resolves`/`resolved`) followed by `#`; the opt-out markers `Refs #` / `No related issue:` leave the body untouched. +## Open linked PRs + +For `/work-items:work` selection — report whether item `` already has an open PR targeting it +for closure, so a candidate whose work is in flight is dropped from the pickable frontier rather +than re-picked. `--search " in:body"` scopes the read to the open PRs that mention the number, +so it is truncation-safe per item (no page-size race — see the `--limit` note under "List items"), +then the closing-keyword test keeps only the ones that actually close `#` (bare read): + +```bash +gh pr list --state open --search " in:body" --json number,body --limit 100 | tr -d '\r' \ + | jq --arg n "" 'any(.[]; .body | test( + "(?i)(?:clos(?:e|es|ed)|fix(?:|es|ed)|resolv(?:e|es|ed)):?[ \t]+#\($n)(?![0-9])"))' +``` + +Emits `true` when at least one open PR closes `#`, `false` otherwise. The **closing keyword is +the authoritative signal**, not the head-branch name: the `pr-issue-linkage` pre-create gate +guarantees a standard-flow PR carries a `Closes #` keyword, so keyword-matching catches it, +while an intentional opt-out (`Refs #` / `No related issue:`) correctly does NOT match and so +does not exclude its issue. This is the same keyword set the "PR closing-keyword mechanics" section +matches. The exact-number boundary (`(?![0-9])`) keeps `#463` from matching `#4630` / `#1463`; the +`#` anchor already prevents a match inside `#1463`. + ## Aggregate / count (dashboard + hygiene) `gh issue list --json ... --jq` projections for `stats` and `audit` (bare `gh`). From 16866ff220606c9c43559d4c0deb03c7d4468dd8 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:03:43 -0400 Subject: [PATCH 2/4] fix(work-items): fully spell closing-keyword regex to clear typos false positive The `clos(?:e|es|ed)` regex fragment in the "Open linked PRs" adapter mechanic tripped the `typos` spell-checker on the standalone `clos` token. Fully spelling the alternation (close|closes|closed|fix|fixes| fixed|resolve|resolves|resolved) is semantically identical (verified equivalent against live PR data), removes the false positive at the root without a suppression pragma or a managed-config edit, and matches the sibling "PR closing-keyword mechanics" section's keyword phrasing. Co-Authored-By: Claude Sonnet 5 --- .../tools/work-item-tracker/adapters/github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md index 2ca2d463d..2e759476d 100644 --- a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md +++ b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md @@ -166,7 +166,7 @@ then the closing-keyword test keeps only the ones that actually close `#` (ba ```bash gh pr list --state open --search " in:body" --json number,body --limit 100 | tr -d '\r' \ | jq --arg n "" 'any(.[]; .body | test( - "(?i)(?:clos(?:e|es|ed)|fix(?:|es|ed)|resolv(?:e|es|ed)):?[ \t]+#\($n)(?![0-9])"))' + "(?i)(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?[ \t]+#\($n)(?![0-9])"))' ``` Emits `true` when at least one open PR closes `#`, `false` otherwise. The **closing keyword is From 0d04aa1f328863bd7f9703595e0968d62e701a6c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:12:41 -0400 Subject: [PATCH 3/4] fix(work-items): address review findings on open-linked-PR mechanic (#463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real findings from the PR review, both in the "Open linked PRs" adapter mechanic: 1. Missing word boundary — the keyword alternation had no lookbehind, so `fix` matched inside ordinary words (a body reading `prefixes #463` spuriously matched `fixes #463` and silently dropped the issue from the frontier). Add `(? in:body"` is a coarse prefilter matching every PR mentioning the digit, so a small `` (e.g. #5) can exceed the page cap and truncate the real closing PR (GitHub sorts by relevance, not recency). Raise the limit to 1000 (exceeds any realistic repo's open-PR count) and reword the prose to describe the prefilter/superset accurately instead of overclaiming. Co-Authored-By: Claude Sonnet 5 --- .../adapters/github/README.md | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md index 2e759476d..90c9fcd27 100644 --- a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md +++ b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md @@ -159,14 +159,19 @@ Match GitHub's issue-closing keyword set (`close`/`closes`/`closed`/`fix`/`fixes For `/work-items:work` selection — report whether item `` already has an open PR targeting it for closure, so a candidate whose work is in flight is dropped from the pickable frontier rather -than re-picked. `--search " in:body"` scopes the read to the open PRs that mention the number, -so it is truncation-safe per item (no page-size race — see the `--limit` note under "List items"), -then the closing-keyword test keeps only the ones that actually close `#` (bare read): +than re-picked. `--search " in:body"` is a coarse prefilter: it returns every open PR whose +body mentions the number — a superset, since a small `` (e.g. `#5`) appears in many PR bodies +and GitHub sorts search by relevance, not recency. `--limit` MUST therefore exceed the repo's +open-PR count so the real closing PR is never truncated away before the precise filter runs +(`--limit 1000` covers any realistic repo; page with `--search` date ranges if a repo ever holds +more open PRs than that — see the `--limit` note under "List items"). The `jq` `test` over the +returned bodies is the authoritative match, keeping only PRs that carry a real closing keyword for +`#` (bare read): ```bash -gh pr list --state open --search " in:body" --json number,body --limit 100 | tr -d '\r' \ +gh pr list --state open --search " in:body" --json number,body --limit 1000 | tr -d '\r' \ | jq --arg n "" 'any(.[]; .body | test( - "(?i)(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?[ \t]+#\($n)(?![0-9])"))' + "(?i)(?`, `false` otherwise. The **closing keyword is @@ -174,8 +179,11 @@ the authoritative signal**, not the head-branch name: the `pr-issue-linkage` pre guarantees a standard-flow PR carries a `Closes #` keyword, so keyword-matching catches it, while an intentional opt-out (`Refs #` / `No related issue:`) correctly does NOT match and so does not exclude its issue. This is the same keyword set the "PR closing-keyword mechanics" section -matches. The exact-number boundary (`(?![0-9])`) keeps `#463` from matching `#4630` / `#1463`; the -`#` anchor already prevents a match inside `#1463`. +matches. Two boundaries keep the match precise: the leading `(?` / `suffix #` does not spuriously exclude the issue; the trailing +`(?![0-9])` number boundary keeps `#463` from matching `#4630` / `#1463` (the `#` anchor already +prevents a match inside `#1463`). ## Aggregate / count (dashboard + hygiene) From 828669a67b86d2de5ea25fdd2bef8146c5b41162 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:49:53 -0400 Subject: [PATCH 4/4] fix(work-items): strip fenced code blocks before closing-keyword scan (#463) The open-linked-PR frontier filter ran its closing-keyword regex over the raw PR body, so a `Closes #` shown only inside a fenced example snippet matched and spuriously dropped a still-ready issue from the pickable frontier. GitHub treats fenced code blocks as inert (they never auto-close an issue), per the pull-request create contract, so scanning them contradicts real closing linkage. Strip fenced blocks (triple-backtick and ~~~, via a backreferenced gsub) before the keyword test, and document the guard alongside the existing word- and number-boundary guards. Addresses the round-3 Codex P2 finding. Co-Authored-By: Claude Sonnet 5 --- .../work-item-tracker/adapters/github/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md index 90c9fcd27..07876e8b7 100644 --- a/plugins/work-items/tools/work-item-tracker/adapters/github/README.md +++ b/plugins/work-items/tools/work-item-tracker/adapters/github/README.md @@ -170,7 +170,7 @@ returned bodies is the authoritative match, keeping only PRs that carry a real c ```bash gh pr list --state open --search " in:body" --json number,body --limit 1000 | tr -d '\r' \ - | jq --arg n "" 'any(.[]; .body | test( + | jq --arg n "" 'any(.[]; (.body | gsub("(?s)(\\x60{3}|~{3}).*?\\1"; "")) | test( "(?i)(?` keyword, so keyword-matching catches it, while an intentional opt-out (`Refs #` / `No related issue:`) correctly does NOT match and so does not exclude its issue. This is the same keyword set the "PR closing-keyword mechanics" section -matches. Two boundaries keep the match precise: the leading `(?` / `suffix #` does not spuriously exclude the issue; the trailing -`(?![0-9])` number boundary keeps `#463` from matching `#4630` / `#1463` (the `#` anchor already -prevents a match inside `#1463`). +matches. Three guards keep the match precise: the leading `gsub` strips fenced code blocks +(triple-backtick or `~~~`) before matching, so a `Closes #` shown only inside an example snippet +does not spuriously exclude the issue — GitHub treats fenced blocks as inert, so such a snippet +never auto-closes anything; the `(?` / +`suffix #` does not spuriously exclude the issue; the trailing `(?![0-9])` number boundary keeps +`#463` from matching `#4630` / `#1463` (the `#` anchor already prevents a match inside `#1463`). ## Aggregate / count (dashboard + hygiene)