Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/work-items/fix-repoint-blockers-on-close-4b7e1c2a.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 25 additions & 3 deletions plugins/work-items/reference/issue-conventions.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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. 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
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 <dependent> --blocked-by <carrier>`.
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.
2 changes: 1 addition & 1 deletion plugins/work-items/skills/track/actions/done.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #<num>`, 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.

Expand Down
2 changes: 2 additions & 0 deletions plugins/work-items/skills/triage/context/apply-outcome.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ref>` body section (`#<M>` same-repo, qualified `<owner>/<repo>#<M>` 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 #<carrier> decision` comment, **never a per-member human-gated label**. Resolving the carrier's decision unblocks the whole cluster in one human touch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,39 @@ 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 `<N>` blocks over
REST, which runs in a cloud session too and keeps the cross-repo dependents the `--json blocking`
projection drops (see "Gotchas"):

```bash
set -o pipefail
gh api --paginate "repos/<owner>/<repo>/issues/<N>/dependencies/blocking" \
--jq '.[] | select(.state == "open") | [.number, .html_url] | @tsv' | tr -d '\r'
Comment thread
kyle-sexton marked this conversation as resolved.
Resolved
```

A non-zero exit means the list may be empty or partial, not that `<N>` 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 `<N>`,
naming the dependent in its own repository (taken from its `html_url`):

```bash
gh issue edit <dependent> --repo <dependent-owner>/<dependent-repo> --remove-blocked-by <N-url>
```

`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);
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. `<M>` may be an issue number or URL:
Expand Down
Loading