diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 23d58cd9..4f8f5ea2 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 66e3479e..a3a78bdb 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 abfc1d15..d5930068 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 285ad82a..07876e8b 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,38 @@ 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"` 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 1000 | tr -d '\r' \ + | jq --arg n "" 'any(.[]; (.body | gsub("(?s)(\\x60{3}|~{3}).*?\\1"; "")) | test( + "(?i)(?`, `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. 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) `gh issue list --json ... --jq` projections for `stats` and `audit` (bare `gh`).