From 116724b42c03a5276bcd5f3981a79e1621e0e5e1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:01:18 -0500 Subject: [PATCH 1/2] fix(work-items): re-point blocked items before closing one Closing an item resolved every edge where it blocked another item, so a duplicate close freed dependents still waiting on the surviving work (#12727 closed as a duplicate, #12621 admitted before its prerequisite). The close paths in triage's apply-outcome and track done now run one shared step, "Dependents on close" in reference/issue-conventions.md: list the open items the closing item blocks, add a blocked-by edge to the item that now carries the work through the seam's link-blocks, then remove the edge to the closing item, and only then close. A delivered close keeps its edges; a won't-fix with no carrier keeps them and names the dependents for re-triage. The GitHub adapter README carries the REST listing and the edge removal under "Close item". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MWmASMaG92q8enDXFa5Doc --- .../fix-repoint-blockers-on-close-4b7e1c2a.md | 7 +++++ .../work-items/reference/issue-conventions.md | 28 +++++++++++++++++-- .../work-items/skills/track/actions/done.md | 2 +- .../skills/triage/context/apply-outcome.md | 2 ++ .../adapters/github/README.md | 27 ++++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 .changes/work-items/fix-repoint-blockers-on-close-4b7e1c2a.md diff --git a/.changes/work-items/fix-repoint-blockers-on-close-4b7e1c2a.md b/.changes/work-items/fix-repoint-blockers-on-close-4b7e1c2a.md new file mode 100644 index 00000000000..f01c63f8b7f --- /dev/null +++ b/.changes/work-items/fix-repoint-blockers-on-close-4b7e1c2a.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- Closing an item through `/work-items:triage` or `/work-items:track done` first moves each open item it blocks onto the item that now carries the work (for a duplicate, the surviving item), then drops the old edge, so a close no longer frees work still waiting on a prerequisite; a won't-fix close keeps its edges and names its dependents for re-triage. diff --git a/plugins/work-items/reference/issue-conventions.md b/plugins/work-items/reference/issue-conventions.md index 19e461d4300..b7045609e80 100644 --- a/plugins/work-items/reference/issue-conventions.md +++ b/plugins/work-items/reference/issue-conventions.md @@ -1,8 +1,9 @@ # Issue conventions -The shape of a well-formed tracker item: title, body, type, labels, close reason. This document is -the **single source of truth for the title convention**; for the other four it **points** at the -existing owner rather than restating it, so each of those rules stays in one place. +The shape of a well-formed tracker item: title, body, type, labels, close reason, and the +dependents a close must re-point. This document is the **single source of truth for the title +convention and the dependents step**; for the other four it **points** at the existing owner +rather than restating it, so each of those rules stays in one place. ## Title @@ -42,3 +43,24 @@ The issue type resolves through `track add`'s type-resolution step (native Issue `completed` vs `not planned`: decided-against and superseded items take `not planned`; a duplicate takes the provider's native `duplicate` reason where it has one, `not planned` otherwise. This follows the `done` action's close discipline: [`../skills/track/actions/done.md`](../skills/track/actions/done.md). + +## Dependents on close + +Every close runs these steps first, so closing an item never frees an item that was waiting on +work still to come: + +1. **List the open items it blocks** (provider mechanic; GitHub: the adapter's "Close item" + section). None: close. +2. **Name the carrier** of the closing item's work. Delivered (a `completed` close, or already + implemented): no carrier, close with the edges as they are. Duplicate: the surviving item or + items that hold the work each dependent waits on. Superseded, or scope moved or split into + other items: the items that now carry it. Dropped with no carrier (won't fix, decided against): + keep the edges, which keep each dependent blocked, and name the dependents in the closing + comment so they are re-triaged. A dependent is never its own carrier. +3. **Add, then remove, then close.** For each dependent and carrier, add the edge through the seam: + `bash "${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/work-item-tracker.sh" link-blocks --blocked-by `. + Only after it lands, remove the dependent's edge to the closing item (provider mechanic, same + section): a won't-do blocker keeps counting as open, so a stale edge would hold the dependent + off the frontier for good (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` + `blocked_by_count`). When any add or removal fails, do not close; report the item and the + dependent. diff --git a/plugins/work-items/skills/track/actions/done.md b/plugins/work-items/skills/track/actions/done.md index f60ea6c4003..6766d7703a0 100644 --- a/plugins/work-items/skills/track/actions/done.md +++ b/plugins/work-items/skills/track/actions/done.md @@ -48,7 +48,7 @@ The summary is institutional memory someone reads later, so lead with what was d - **This item's `closes` line is present (or the keyword step just added it)** → do NOT close manually: post the completion summary as a plain comment (adapter: "Comment on item") and report "will auto-close when #{pr} merges". - **The PR body carries a deliberate opt-out for THIS item (its `refs` line, any other opt-out marker the adapter "PR closing-keyword mechanics" section lists such as GitHub's `Refs #`, or `No related issue:`), or `closes` is `null`, so a merge will NOT close it** → do NOT claim auto-close. Post the completion comment and leave the item open, reporting that #{pr} will not close it. For an opt-out, which was intentional, ask the user if they actually want it closed; for a `null` `closes`, close it with this action once the PR merges. A closing line for a *different* item never counts as this item's auto-close. - Closing now would mark the item done before the work has landed. A failed or abandoned PR would leave it wrongly closed. Close directly (adapter: "Close item", WRITE via the adapter's identity policy), passing the closing comment and `--reason completed` (or `not planned` for `--not-planned`), ONLY when there is no `--pr` or the named PR has already merged. + Closing now would mark the item done before the work has landed. A failed or abandoned PR would leave it wrongly closed. Before any direct close, run "Dependents on close" in [`reference/issue-conventions.md`](../../../reference/issue-conventions.md); a `completed` close is the delivered case. Close directly (adapter: "Close item", WRITE via the adapter's identity policy), passing the closing comment and `--reason completed` (or `not planned` for `--not-planned`), ONLY when there is no `--pr` or the named PR has already merged. The seam claim is a lease (assignee + lease comment), not a label. Closing removes the item from the frontier, so no `status:*` label cleanup is part of this flow. diff --git a/plugins/work-items/skills/triage/context/apply-outcome.md b/plugins/work-items/skills/triage/context/apply-outcome.md index ffc228ee54c..af53f65889f 100644 --- a/plugins/work-items/skills/triage/context/apply-outcome.md +++ b/plugins/work-items/skills/triage/context/apply-outcome.md @@ -28,6 +28,8 @@ Every outcome is a **transition off raw**, not a layer on top of it. Applying an | Won't fix (enhancement) | Close with rationale comment; when the repo keeps `docs/out-of-scope/`, record the rejection in the matching concept file (re-read + append to "Prior requests", or create the concept file for a first rejection) and link it from the closing comment. An enhancement PR gets the same record as an issue, so a later PR for the rejected idea meets the earlier decision | | Duplicate | Never `completed`. Close via the adapter's native duplicate mechanic when the provider has one (GitHub: `--duplicate-of`), else not-planned + a `## Duplicate of ` body section (`#` same-repo, qualified `/#` or URL cross-repo) + link comment | +**Every close outcome re-points dependents first.** Before the close, run "Dependents on close" in [`reference/issue-conventions.md`](../../../reference/issue-conventions.md): a duplicate's dependents move to the surviving item, a won't-fix keeps its edges and names its dependents in the closing comment, and already implemented closes as delivered. + For a PR, the outcome addresses the attached code explicitly: adopt the diff (briefed for an agent or human to carry forward), rework it (brief describes the gap between the diff and the verified requirement), or decline it (close with rationale, and the ledger entry when it's a rejected enhancement). **Decision-carrier clusters.** When step 1's cluster detection found members sharing one decision, apply human-gated to the **carrier only** (its body lists the member numbers). Each other member instead gets a native `blocked-by` edge to the carrier plus a `blocked by # decision` comment, **never a per-member human-gated label**. Resolving the carrier's decision unblocks the whole cluster in one human touch. 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 0fbcf6e0edb..7fbb00a9b55 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 @@ -243,6 +243,33 @@ The `done` action closes with `--reason completed`, and with `--reason "not plan [gh issue close § Options](https://cli.github.com/manual/gh_issue_close#options) (as of 2026-11-03; recheck when that section changes the `--reason` values). +**Re-point dependents first.** The steps and their order are in +[`reference/issue-conventions.md`](../../../../reference/issue-conventions.md) "Dependents on +close"; the GitHub reads and writes they name are these. List the open items `` blocks over +REST, which runs in a cloud session too and keeps the cross-repo dependents the `--json blocking` +projection drops (see "Gotchas"): + +```bash +gh api --paginate "repos///issues//dependencies/blocking" \ + --jq '.[] | select(.state == "open") | [.number, .html_url] | @tsv' | tr -d '\r' +``` + +After the seam's `link-blocks` adds a dependent's edge to its carrier, remove its edge to ``, +naming the dependent in its own repository (taken from its `html_url`): + +```bash +gh issue edit --repo / --remove-blocked-by +``` + +`link-blocks` exits 3 in a cloud session ("Sandboxed sessions" under "View item"), so an item +with open dependents is not closed there; report it for a session that can write the edges. + +- **Pointer**: the endpoint at + [issue dependencies § List dependencies an issue is blocking](https://docs.github.com/en/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocking); + the flag at [gh issue edit § Options](https://cli.github.com/manual/gh_issue_edit#options). + **As of**: 2026-11-03. **Recheck trigger**: that section changes the path or the response's + `state` or `html_url`, or a gh release renames `--remove-blocked-by`. + **Duplicate close with native `--duplicate-of`.** GitHub closes a duplicate natively: this sets close reason `duplicate` and a structured, API-queryable `duplicateOf` relationship, strictly better than grepping a body header. `` may be an issue number or URL: From 2343cc4332ba1357970dacf187d8c50fca12f37f Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:16:22 -0500 Subject: [PATCH 2/2] fix(work-items): stop a close on a failed dependents read The dependents listing piped through tr, so a failed or partial REST read exited 0 with an empty list and the close went ahead. The recipe now sets pipefail and a non-zero exit stops the close. The cloud-session refusal now covers only a close that must move an edge to a carrier; a close that keeps its edges goes ahead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MWmASMaG92q8enDXFa5Doc --- plugins/work-items/reference/issue-conventions.md | 2 +- .../tools/work-item-tracker/adapters/github/README.md | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/work-items/reference/issue-conventions.md b/plugins/work-items/reference/issue-conventions.md index b7045609e80..1137f74ccd1 100644 --- a/plugins/work-items/reference/issue-conventions.md +++ b/plugins/work-items/reference/issue-conventions.md @@ -50,7 +50,7 @@ Every close runs these steps first, so closing an item never frees an item that work still to come: 1. **List the open items it blocks** (provider mechanic; GitHub: the adapter's "Close item" - section). None: close. + section). None: close. A failed or partial read is not none: do not close. 2. **Name the carrier** of the closing item's work. Delivered (a `completed` close, or already implemented): no carrier, close with the edges as they are. Duplicate: the surviving item or items that hold the work each dependent waits on. Superseded, or scope moved or split into 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 7fbb00a9b55..c2fb7a255a0 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 @@ -250,10 +250,14 @@ REST, which runs in a cloud session too and keeps the cross-repo dependents the projection drops (see "Gotchas"): ```bash +set -o pipefail gh api --paginate "repos///issues//dependencies/blocking" \ --jq '.[] | select(.state == "open") | [.number, .html_url] | @tsv' | tr -d '\r' ``` +A non-zero exit means the list may be empty or partial, not that `` blocks nothing: stop the +close and report it. + After the seam's `link-blocks` adds a dependent's edge to its carrier, remove its edge to ``, naming the dependent in its own repository (taken from its `html_url`): @@ -261,8 +265,10 @@ naming the dependent in its own repository (taken from its `html_url`): gh issue edit --repo / --remove-blocked-by ``` -`link-blocks` exits 3 in a cloud session ("Sandboxed sessions" under "View item"), so an item -with open dependents is not closed there; report it for a session that can write the edges. +`link-blocks` exits 3 in a cloud session ("Sandboxed sessions" under "View item"), so a close +whose dependents need a new carrier is not made there; report it for a session that can write the +edges. A close that keeps its edges (delivered, or dropped with no carrier) needs no edge write +and goes ahead. - **Pointer**: the endpoint at [issue dependencies § List dependencies an issue is blocking](https://docs.github.com/en/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocking);