diff --git a/.changes/context-budget/work-resolve-issue-issue-12689-41587864264-1-6441cda6.md b/.changes/context-budget/work-resolve-issue-issue-12689-41587864264-1-6441cda6.md new file mode 100644 index 00000000000..ee53c02b366 --- /dev/null +++ b/.changes/context-budget/work-resolve-issue-issue-12689-41587864264-1-6441cda6.md @@ -0,0 +1,12 @@ +--- +bump: patch +--- + +### Changed + +- The README, `/context-budget:audit` and its engine and report references no longer restate + upstream behavior: what `/context` itemizes, the skill-frontmatter subtraction from + `System tools` (now recorded as the engine's own observation), the bundled `explain-usage` + skill's description, the skill listing budget's unit, and what `/skill-doctor` and `/doctor` + cover. Each keeps its own rule and links the upstream section, or the local record where no + upstream page documents the fact, with an as-of date and a recheck trigger. diff --git a/.changes/work-items/work-resolve-issue-issue-12689-41587864264-1-f7210283.md b/.changes/work-items/work-resolve-issue-issue-12689-41587864264-1-f7210283.md new file mode 100644 index 00000000000..a65f10c57cd --- /dev/null +++ b/.changes/work-items/work-resolve-issue-issue-12689-41587864264-1-f7210283.md @@ -0,0 +1,15 @@ +--- +bump: patch +--- + +### Changed + +- The permission preflight reference, the tracker seam reference, `/work-items:attend-queue`, + `/work-items:setup`, the work-loop telemetry upsert reference and the GitHub adapter README no + longer restate upstream behavior: permission rule matching, when git sets `core.worktree`, the + `jq` install on Windows, `gh` flag spellings, `--limit` defaults, `--reason` values, `gh api` + field and placeholder handling, the sub-issue `--json` projection, GitHub search qualifiers, REST + pagination and assignee responses, closing keywords and linkage, `includeClosedPrs`, + cloud-session API refusals and issue form labels. Each keeps its own rule and links the upstream + section with an as-of date and a recheck trigger. The adapter README's "Edit labels / assignees" + recipe now shows the `--remove-assignee "@me"` call attend-queue and triage use. diff --git a/plugins/context-budget/README.md b/plugins/context-budget/README.md index 72d6fd11eff..c6eaec21015 100644 --- a/plugins/context-budget/README.md +++ b/plugins/context-budget/README.md @@ -3,11 +3,13 @@ Measure a Claude Code session's fixed startup context payload **per item**, on your machine, at a pinned binary, and record what every trim actually saved. -Per-skill, per-agent, and per-MCP-tool attribution stays with the built-in `/context` command. -This plugin covers what our measurements see `/context` report only as totals: the built-in tool -pools, `System tools` and `System tools (deferred)`. Together they are typically the largest -single contributor to the fixed payload, which is why the plugin targets them. It attributes them -per tool by A/B differencing: a baseline headless session versus one session per candidate +This plugin attributes the built-in tool pools, `System tools` and `System tools (deferred)`, per +tool. Together they are typically the largest single contributor to the fixed payload, which is +why the plugin targets them. The rest of the breakdown stays with the built-in `/context` command; +what it itemizes is read at its row in +[commands § All commands](https://code.claude.com/docs/en/commands#all-commands) +(as of 2026-11-03; recheck when that row changes what `/context` itemizes). The plugin attributes +the pools by A/B differencing: a baseline headless session versus one session per candidate tool with that tool denied by bare name. The two attributed buckets compose differently: deferred-side deltas add, so a basket's deferred saving is the sum of its members, while prefix-side deltas double-count, so their sum is only an upper bound on the basket's prefix @@ -192,8 +194,8 @@ tool needs its own probe, and NotebookEdit was not probed. - **Every report is stamped** with the measured binary path and version, the measurement mode, and the session kind. Machines with two CLI installs get an answer per binary, not a blend. - **Comparability is enforced, not advised.** `System tools` deltas are only valid between runs - with identical skill listings (listed skill frontmatter is subtracted from that bucket); the - engine fingerprints the listing per run and marks violating comparisons incomparable rather + with identical skill listings (why: `skills/audit/reference/engine.md` "Comparability rules"); + the engine fingerprints the listing per run and marks violating comparisons incomparable rather than reporting their numbers. - **Levers are catalogued data.** Each row in `skills/audit/reference/levers.json` carries its honesty category (does it remove weight, work diff --git a/plugins/context-budget/skills/audit/SKILL.md b/plugins/context-budget/skills/audit/SKILL.md index 69ab76adbe8..b0e00e9bc43 100644 --- a/plugins/context-budget/skills/audit/SKILL.md +++ b/plugins/context-budget/skills/audit/SKILL.md @@ -75,9 +75,10 @@ Two rules govern everything this skill says, per the plugin's One native surface also answers "what is using my tokens", and the two get conflated when a session feels crowded: -- **`explain-usage` (bundled skill)**: after the fact, explains where the current session's tokens - went, with one simple chart in plain language. The model and the person can both invoke it where - it resolves. +- **`explain-usage` (bundled skill)**: what it does and who can invoke it are recorded in + [`reference/native-explain-usage.md`](reference/native-explain-usage.md). No upstream page + documents it: the commands and skills pages and the docs index carry no entry (as of 2026-11-03; + recheck when the commands page gains an `/explain-usage` row or a release note names it). - **This skill (marketplace plugin).** Measures the fixed startup payload of a fresh headless session per item, including the built-in tool pools `/context` shows only as totals, and keeps a before/after ledger for every lever the operator toggles. @@ -334,10 +335,9 @@ otherwise use the payload's share of the measured window. Each of these produces a confidently wrong number unless the engine's guard is honored: -- **Removing skills makes `System tools` rise.** Listed skill-frontmatter tokens are subtracted - from that bucket, so a run that changes the skill listing shifts `System tools` with no tool - changing state; read naively, a safe-mode run looks as if safe mode loads deferred tools. The - signature check catches this; never hand-compare two snapshots the engine marked incomparable. +- **`System tools` moves with the skill listing.** The signature check catches a run whose + listing differed (why: [`reference/engine.md`](reference/engine.md) "Comparability rules"); + never hand-compare two snapshots the engine marked incomparable. - **Unredirected stdin prepends a warning line** to headless output, which breaks naive parsing. The engine redirects and strips; if you capture `/context` by hand for `parse-context`, redirect stdin or expect the leading line. diff --git a/plugins/context-budget/skills/audit/reference/engine.md b/plugins/context-budget/skills/audit/reference/engine.md index bda3d9ab207..e0abebe0b65 100644 --- a/plugins/context-budget/skills/audit/reference/engine.md +++ b/plugins/context-budget/skills/audit/reference/engine.md @@ -60,14 +60,17 @@ Each row is our decision; what the upstream section says about it is read live a record the last row relays changes its pointer or its heading moves. Where the engine's behavior rests on empirical observation rather than documentation (headless -`/context`, the skill-listing subtraction below), the record says so in `caveats`. The engine +`/context`, the skill-listing signature below), the record says so in `caveats`. The engine reports the observation and never silently assumes it durable. ## Comparability rules (enforced, not advisory) -1. **Skill-listing signature.** The prefix `System tools` bucket has listed skill-frontmatter - tokens subtracted from it, so its value is only meaningful relative to a run with an identical - skill listing. Every snapshot carries `skillListing.signature`, a hash of the sorted +1. **Skill-listing signature.** The prefix `System tools` value is treated as meaningful only + relative to a run with an identical skill listing, because the engine observes it moving with + the listing. That is an observation, not documentation: the `/context` row in + [commands § All commands](https://code.claude.com/docs/en/commands#all-commands) does not + itemize the bucket (as of 2026-11-03; recheck when that row documents what `System tools` + counts). Every snapshot carries `skillListing.signature`, a hash of the sorted (name, source) listing. On a mismatch, `compare`/`attribute` mark `systemToolsComparable: false` and put the reason in `comparability.reasons`. The deferred bucket is a separate pool and listing or Skills-token drift does not poison it: per-bucket additivity applies this gate diff --git a/plugins/context-budget/skills/audit/reference/report.md b/plugins/context-budget/skills/audit/reference/report.md index c5be4e09c93..6de16b03878 100644 --- a/plugins/context-budget/skills/audit/reference/report.md +++ b/plugins/context-budget/skills/audit/reference/report.md @@ -34,8 +34,8 @@ mode; the displayed fraction in cli-parse mode). not measurable rather than as not additive. Unmeasured tools (candidates this run that were not priced) are listed as unmeasured, not omitted. Silence reads as "measured zero". Some tools exist only in an interactive session; [`interactive-only-tools.json`](interactive-only-tools.json) - names them, plus the interactive-only MCP server class (relayed as of 2026-10-11; recheck this - relay when that file moves or its `tools` list or `notes` change). These are listed as + names them, plus the interactive-only MCP server class (relayed as of 2026-10-11; recheck when + that file moves or its `tools` list or `notes` change). These are listed as **known-uncovered**, a distinct category from unmeasured-but-candidate, because they were never candidates in this headless sweep. The names come from the attribution record's `knownUncovered.tools` (product-level surfaces) plus `knownUncovered.notes` (the @@ -48,14 +48,16 @@ mode; the displayed fraction in cli-parse mode). measured resolution where the row has one), the measured or measurable delta, the exact emitted config, and the official citation. A row with `saving: "runtime-resolved"` does not print its stored category as the saving; print the measured resolution, including an explained - zero when the listing cap is saturated. The listing cap is characters, not the Skills token - total. A row with `measurementScope: "unmeasurable-in-this-session-kind"` is its own group, + zero when the listing cap is saturated; what the cap counts is read at + [skills § Skill descriptions are cut short](https://code.claude.com/docs/en/skills#skill-descriptions-are-cut-short) + (as of 2026-11-03; recheck when that section changes the listing budget's unit). A row with `measurementScope: "unmeasurable-in-this-session-kind"` is its own group, "unmeasurable in this session kind", and is not reported as a measured zero. Grouped by category, `removes-weight` first. Postures bind: `never-recommend` rows appear under a "priced, not recommended" heading; `report-only` vendor weight closes the group as the honest floor. -6. **Routes.** The catalogue's route-outs (`/skill-doctor` for unused skills and `/doctor` for - unused MCP servers and plugins, both of which the operator runs; memory files, hooks, live occupancy to their owners), each in one line. +6. **Routes.** The catalogue's route-outs (`/skill-doctor` and `/doctor`, both of which the + operator runs, routed and pointed at in [SKILL.md](../SKILL.md) "Scope boundary (route out)"; + memory files, hooks, live occupancy to their owners), each in one line. 7. **Degradations and caveats.** Every `caveats[]` entry from the records used, plus anything the engine could not measure and why. The attribution record already merges the baseline with each deny run and the combined additivity run, so a deny-run disclosure is in that list and diff --git a/plugins/work-items/reference/permission-preflight.md b/plugins/work-items/reference/permission-preflight.md index 1cdd53bebd3..4e99dbd179e 100644 --- a/plugins/work-items/reference/permission-preflight.md +++ b/plugins/work-items/reference/permission-preflight.md @@ -100,12 +100,13 @@ is interpreter-led, so no allow rule pre-approves it permission mode decides it, the auto-mode classifier in an unattended lane. It reads the effective `permissions.allow` from the operator's user-global settings and the project settings. It never runs a live permission probe. A verb counts as covered only by an **open-glob** grant: `Bash(git commit *)` or `Bash(git commit:*)`. A **bare-exact** rule -(`Bash(git commit)`) is **not** coverage: it permits only the argumentless command, and a work-lane -invocation always carries arguments, so the real call would still prompt. When a gapped verb has -*only* a bare-exact grant, the gap message says so precisely. That grant does cover an argumentless -call but not the work lane's argument-carrying one, and the remedy is to add the open glob -(`git commit *`). A narrower, flag-scoped rule -(`Bash(git commit --amend)`) is likewise not coverage. A missing +(`Bash(git commit)`) or a narrower, flag-scoped one (`Bash(git commit --amend)`) is **not** +coverage, because a work-lane invocation always carries arguments. When a gapped verb has *only* a +bare-exact grant, the gap message names it, and the remedy is to add the open glob +(`git commit *`). What each rule shape matches is read live at +[permissions § Wildcard patterns](https://code.claude.com/docs/en/permissions#wildcard-patterns) +(as of 2026-11-03; recheck when that section changes what a rule with no `*`, or with a trailing +`*`, matches). A missing verb means the floor is not composed in on this machine; the remediation is to apply the component operator-side, not to add a one-off rule. @@ -168,9 +169,12 @@ one). The candidates: 1. **The probed checkout itself**, whose own git dir *is* the common dir. True whatever the git dir is named, so the **main checkout of** a `--separate-git-dir` or submodule layout resolves here. The probed checkout is `--project-root` when given, else the cwd. -2. **`core.worktree`** in the common dir's config, resolved relative to that dir. Git writes it for - submodules, which is what makes a submodule's `/.git/modules/` common dir resolvable at - all, since no parent-of-`.git` arithmetic can invert it. +2. **`core.worktree`** in the common dir's config, resolved relative to that dir. It is the + candidate for a submodule's `/.git/modules/` common dir, which no parent-of-`.git` + arithmetic can invert. Where git sets the key, and how a relative value resolves, is read live + at [git-config § core.worktree](https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreworktree) + (as of 2026-11-03; recheck when that entry changes how a relative value resolves or what + overrides it). 3. **The conventional `/.git` spelling**, the parent of the common dir. Resolution has three outcomes, and the report distinguishes them: @@ -215,9 +219,8 @@ that worktree. Two cases: in silence.** For a repository created with `--separate-git-dir /.git`, the main working tree is the directory holding the `.git` *file*, but git records no back-pointer to it. A recursive search of the whole common dir turns up only `worktrees//gitdir` entries, which point at *linked* - worktrees; `core.worktree` is unset by every creation path (`git init --separate-git-dir` on a - fresh directory, `git clone --separate-git-dir`, and `git init --separate-git-dir` over an existing - repository). Git's own `git worktree list` therefore reports `` as the main worktree, and + worktrees, and the `core.worktree` candidate resolves only where that key is set (git's entry + above). Git's own `git worktree list` therefore reports `` as the main worktree, and reports it identically when run *from* that true working tree. That `` is the parent of the separate git *directory*, not of the `.git` file, which lives in the true working tree and is the very link git does not record in reverse. `` diff --git a/plugins/work-items/reference/tracker-seam.md b/plugins/work-items/reference/tracker-seam.md index 10ee0245217..23f0ed6e18f 100644 --- a/plugins/work-items/reference/tracker-seam.md +++ b/plugins/work-items/reference/tracker-seam.md @@ -33,8 +33,8 @@ third, the provider **binding**, is discharged differently: a missing binding bl routes rather than halting unconditionally. - **`jq`** (`command -v jq`). The actions' snippets parse with it unconditionally. Missing: stop - and surface the install remediation (; a separate install under Git - Bash on native Windows). Never improvise a parse. + and surface the install remediation, the per-platform install at + (as of 2026-11-03; recheck when that page moves). Never improvise a parse. - **The seam script**, `"$TRACKER"` above. The plugin bundles it, so it resolves at the plugin-dir path by default; if it resolves at **neither** the plugin path nor the project-root fallback, the plugin install is incomplete: stop and surface that the plugin must be reinstalled or repaired, @@ -96,8 +96,8 @@ is nonetheless authoritative for `parent_id`, which is how an item reaches its c the text of an item, such as a container's Brief or an item's acceptance criteria, is therefore a **provider-mechanic** read (`gh issue view --repo / --json body,title` on GitHub; the provider's REST equivalent otherwise), and a surface that shows a body read must label it as -such rather than folding it into a seam snippet. That GitHub form is GraphQL-backed, so a sandboxed -session (Claude Code on the web, remote execution) reads over REST instead; the adapter's +such rather than folding it into a seam snippet. A sandboxed session (Claude Code on the web, +remote execution) reads that body over REST instead; the adapter's operations reference carries the REST substitute and the pointer record under "View item" (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md`). Provider mechanics run unbound, so the read still @@ -236,9 +236,9 @@ need attention. ## Gotchas Skill-behavior failure patterns. Add to this section when new gotchas are discovered. -Provider-mechanic gotchas (Windows `\r`, search-qualifier syntax, the `gh` 30-row default limit, -`--add-label` vs `--label`, `--reason` values, rate limits, Issue-Forms auto-labeling) live in the -bound adapter's operations reference. For GitHub, that is +Provider-mechanic gotchas (Windows `\r`, search syntax, `gh` list limits, label and assignee edit +flags, close reasons, rate limits, labels on programmatic creation) live in the bound adapter's +operations reference. For GitHub, that is `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md` "Gotchas". - **Unattended loops hold to the provider's API budget.** Every lane, worker and session under one diff --git a/plugins/work-items/skills/attend-queue/SKILL.md b/plugins/work-items/skills/attend-queue/SKILL.md index 17537d40548..f0534ae817d 100644 --- a/plugins/work-items/skills/attend-queue/SKILL.md +++ b/plugins/work-items/skills/attend-queue/SKILL.md @@ -160,11 +160,12 @@ worker lane off the flipped item. Once the row's answer is written - **When the human blocker is removed:** perform the single-edit role-label flip **while this session still holds the claim**, then clear `@me` from assignees via the bound adapter's assignee - edit (`--remove-assignee "@me"` for GitHub. See the adapter README "Edit labels / assignees"), - then end the lease with `"$TRACKER" release "" --lease-comment-id `. Never clear the - assignee before the flip: a released row still reads as `[escalated]` in another - attended session's view until the label lands. A brief frontier delay after the flip is - acceptable. + edit (for GitHub, the adapter README "Edit labels / assignees"; the flag is read live at + [gh issue edit § Options](https://cli.github.com/manual/gh_issue_edit#options), + as of 2026-11-03; recheck when that section renames the assignee flags), then end the lease with + `"$TRACKER" release "" --lease-comment-id `. Never clear the assignee before the flip: a + released row still reads as `[escalated]` in another attended session's view until the label + lands. A brief frontier delay after the flip is acceptable. - **When disposition leaves the item human-gated** (decline, an answered escalation on a human-floor work class that was not reclassified): clear `@me` via the same adapter assignee edit once disposition comments are written, still while holding the claim diff --git a/plugins/work-items/skills/setup/SKILL.md b/plugins/work-items/skills/setup/SKILL.md index 946ff14fcfc..73d2edab29f 100644 --- a/plugins/work-items/skills/setup/SKILL.md +++ b/plugins/work-items/skills/setup/SKILL.md @@ -422,8 +422,8 @@ report INFO naming `/work-items:setup apply` with a user present. `config.github.wont_do_labels`; an empty pick removes the key. - **`jira`: classify resolutions.** Modeled on the role-label pass above: 1. **Read the current binding** and present `config.jira.resolutions` when set. - 2. **Discover** the instance's resolutions with the paginated - `GET /rest/api/3/resolution/search` (the call, its auth and its fields are + 2. **Discover** the instance's resolutions through Jira's resolution search (the call, its + paging, its auth and its fields, with the pointer to Atlassian's Issue resolutions API, are [`reference/providers.md`](reference/providers.md) "`jira`"). Show each resolution's name and description, and mark the default one. 3. **Have the user sort each into `completed` or `wont_do`.** You may pre-select a suggestion diff --git a/plugins/work-items/skills/track/actions/list.md b/plugins/work-items/skills/track/actions/list.md index dd8b966f8eb..8bb61bc6385 100644 --- a/plugins/work-items/skills/track/actions/list.md +++ b/plugins/work-items/skills/track/actions/list.md @@ -35,4 +35,4 @@ The `#` column is a sequential index for this listing. When the user references ## Search syntax -Provider search qualifiers (label/exclude/assignee/sort/date) and the pass-through behavior of `--search` are documented in the bound adapter's operations reference. GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md` "Search items". +Provider search qualifiers (label/exclude/assignee/sort/date) are linked from, and the pass-through behavior of `--search` is documented in, the bound adapter's operations reference. GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md` "Search items". diff --git a/plugins/work-items/skills/track/actions/search.md b/plugins/work-items/skills/track/actions/search.md index 3c8c827bb60..b394cfa3384 100644 --- a/plugins/work-items/skills/track/actions/search.md +++ b/plugins/work-items/skills/track/actions/search.md @@ -54,4 +54,4 @@ If no matches anywhere: "No items matching '{query}' in open, closed, or recurri ## Search syntax -The provider's search qualifiers (label / exclude / assignee / sort / date / exact-phrase) are documented in the bound adapter's operations reference. GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md` "Search items". +The provider's search qualifiers (label / exclude / assignee / sort / date / exact-phrase) are linked from the bound adapter's operations reference. GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md` "Search items". diff --git a/plugins/work-items/skills/work-loop/reference/telemetry-upsert.md b/plugins/work-items/skills/work-loop/reference/telemetry-upsert.md index 3d87c3c3177..a93ea531a5c 100644 --- a/plugins/work-items/skills/work-loop/reference/telemetry-upsert.md +++ b/plugins/work-items/skills/work-loop/reference/telemetry-upsert.md @@ -64,8 +64,9 @@ PATCH that fails leaves the previous cycle's body in place, which a read-back ru would happily accept. The post-write read-back then re-reads what the write stored and compares it with the body file, ignoring only CR and trailing newlines: the only check that sees a write which reported success and stored something else, including an earlier cycle's well-formed body. Create -and update use -`-F body=@`, because `-f body=@FILE` transmits the literal path. +and update pass the body file as `-F body=@`, never `-f`; how each flag treats a leading `@` +is read live at [gh api § Options](https://cli.github.com/manual/gh_api#options) +(as of 2026-11-03; recheck when that section changes how `-f` or `-F` reads a value). ## Exit codes 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 f111750ec74..0fbcf6e0edb 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 @@ -59,8 +59,8 @@ PREFIX="$(gh repo view --json owner,name -q '"github:\(.owner.login)/\(.name)"' ID="${PREFIX}#" ``` -`gh repo view --json` is GraphQL-backed, so it 403s in a sandboxed session for the reason given -under "View item" below. The REST form produces the same string: +In a sandboxed session use the REST form, which produces the same string (the pointer under +"View item" below says which requests those sessions refuse): ```bash PREFIX="$(gh api "repos/{owner}/{repo}" --jq '"github:" + .full_name' | tr -d '\r')" @@ -82,9 +82,10 @@ gh issue list \ ``` Forward `--assignee` for the `list --assignee` flag and the audit's assigned-only view (use -`--assignee "@me"` for the current user). `--limit` is mandatory when more than 30 rows are -needed (`gh` truncates at 30 silently; max page size 100, so for larger sets, page with `--search` -date ranges). +`--assignee "@me"` for the current user). Always pass `--limit` sized to the rows needed; the +default cap is read at [gh issue list § Options](https://cli.github.com/manual/gh_issue_list#options) +(as of 2026-11-03; recheck when that section changes the `--limit` default). Past what one +`--limit` returns, page with `--search` date ranges. **`list-items` in a cloud session.** `gh issue list --json blockedBy` is GraphQL, so with `CLAUDE_CODE_REMOTE=true` the `list-items` verb reads the @@ -106,24 +107,19 @@ gh issue list --search "" --state open --json number,title,state,labels gh issue list --search "" --state closed --json number,title,state,labels,closedAt --limit 10 | tr -d '\r' ``` -Search-qualifier reference: - -| Qualifier | Example | Meaning | -|-----------|---------|---------| -| `label:name` | `label:type:chore` | Has label | -| `-label:name` | `-label:stale` | Excludes label | -| `no:assignee` | `no:assignee` | Unassigned | -| `assignee:login` | `assignee:@me` | Assigned to user | -| `sort:field-dir` | `sort:created-asc` | Sort (created, updated, comments) | -| `created:>date` | `created:>2026-01-01` | Created after date | -| `updated:>date` | `updated:>2026-03-01` | Updated after date | -| `"exact phrase"` | `"fix authentication"` | Body/title text search | - -Multiple qualifiers AND-combine: `label:type:chore label:recurring no:assignee sort:created-asc`. - -**REST form (cloud sessions).** `gh issue list --search` is GraphQL, which cloud -sessions refuse (see "View item" below). We observed REST `search/issues` refused in cloud sessions -too (the "View item" pointer does not cover search), so it is not the fallback. List the +A typical query: `label:type:chore label:recurring no:assignee sort:created-asc`. The qualifiers +are read live at GitHub's searching issues and pull requests page, one section each: +[§ Search by label](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-label), +[§ Search by assignee](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-assignee), +[§ Search by missing metadata](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-missing-metadata), +[§ Search by the title, body, or comments](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments) +and +[§ Search by when an issue or pull request was created or last updated](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-when-an-issue-or-pull-request-was-created-or-last-updated) +(as of 2026-11-03; recheck when one of those sections renames a qualifier). That page leaves +`sort:` to GitHub's sorting search results page, linked from its "Further reading". + +**REST form (cloud sessions).** A cloud session searches with neither `gh issue list --search` nor +REST `search/issues`. List the repo's issues over REST and filter client-side: `` are the query words, all of which must appear in the title, case-insensitively. Pull requests come back from the issues endpoint and are dropped. @@ -166,12 +162,12 @@ gh issue view --json assignees,labels \ --jq '{assignees: [.assignees[].login], labels: [.labels[].name]}' | tr -d '\r' ``` -**Sandboxed sessions: read the item over REST.** `gh issue view --json` routes through GitHub's -GraphQL API, so in a cloud session (Claude Code on the web and remote execution) read the item with -the REST recipe below. It is the same reason the lease protocol's assignee ops use REST under "Edit -labels / assignees" below, and the note every other "sandboxed session" remark in this plugin -cites. A 403 there is not an auth problem, so take it as a signal to switch APIs rather than to -re-authenticate. +**Sandboxed sessions: read the item over REST.** In a cloud session (Claude Code on the web and +remote execution) read the item with the REST recipe below, not `gh issue view --json`, whose API +the GitHub proxy pointer below covers. The lease protocol's assignee ops use REST under "Edit +labels / assignees" below for the same reason, and +every other "sandboxed session" remark in this plugin cites this note. Take a 403 there as a signal +to switch APIs rather than to re-authenticate. The adapter makes that switch itself: with `CLAUDE_CODE_REMOTE=true` (set in cloud sessions) it takes the REST read and create paths whatever the `gh` version, and a native read or create that @@ -183,15 +179,13 @@ there with an "unavailable in this Claude Code cloud session" message. - **Pointer**: when a `gh` call returns 403 in a cloud session, fetch the GraphQL restrictions in [cloud environments § GitHub proxy](https://code.claude.com/docs/en/cloud-environments#github-proxy) - live. **As of**: 2026-10-11. **Recheck trigger**: that section's GraphQL bullet changes, or a + live. **As of**: 2026-11-03. **Recheck trigger**: that section's GraphQL bullet changes, or a release note names the GitHub proxy. -The REST issues endpoint carries -the same fields under the same names. Use the object-array form when substituting -the general `gh issue view --json number,title,body,labels,assignees,comments` -shape (`.labels[].name` / `.assignees[].login` keep working). REST `comments` is -an integer count, not the comment list `--json comments` returns; take comments -from the paginated "List item comments" recipe. +The REST recipe below substitutes for the general +`gh issue view --json number,title,body,labels,assignees,comments` shape (`.labels[].name` / +`.assignees[].login` keep working) except for comments: take those from the paginated "List item +comments" recipe. ```bash gh api "repos/{owner}/{repo}/issues/" \ @@ -207,17 +201,14 @@ gh api "repos/{owner}/{repo}/issues/" \ | tr -d '\r' ``` -`gh api` has no `--repo` flag: `{owner}` and `{repo}` expand from the repository of the current -directory, or from `GH_REPO`. Run it from the target clone, or prefix `GH_REPO=/`. - -`comments` is the one projected field that does not carry over. REST returns it as an integer -count, not the list `--json comments` gives. Take comments from "List item comments" below, which -is already REST and paginates for the reason documented there. +Run `gh api` from the target clone, or prefix `GH_REPO=/`; where its `{owner}` and +`{repo}` placeholders come from is read in the description at the top of +[gh api](https://cli.github.com/manual/gh_api) (as of 2026-11-03; recheck when that description +changes where the placeholders resolve from). **What this substitute does not cover.** The list, search, and aggregate projections below run on -`gh issue list --json`, which posts to `/graphql` like the reads above and carries no REST form -here. Under this restriction they are unavailable, as are the seam verbs named at the end of -"Edit labels / assignees". +`gh issue list --json` and carry no REST form here, so a cloud session does not use them, nor the +seam verbs named at the end of "Edit labels / assignees". ## List item comments @@ -228,9 +219,12 @@ gh api --paginate "repos/{owner}/{repo}/issues//comments?per_page=100" \ | jq -s '[.[][] | {id, user: .user.login, created_at, body}] | sort_by(.id)' | tr -d '\r' ``` -`--paginate` is required. The endpoint returns 30 per page oldest-first and reports nothing -when it truncates, so an unpaginated read silently drops the newest comments, and this repo -already has long-running telemetry items past that count. +`--paginate` is required: an unpaginated read stops at the first page, and this repo already has +long-running telemetry items past one page. Page size and how a response signals further pages +are read at +[using pagination in the REST API § About pagination](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api#about-pagination) +(as of 2026-11-03; recheck when that section changes the default page size or how further pages +are signaled). The reduction is slurped rather than passed to `--jq` because `gh` applies `--jq` to each page separately: `sort_by` there emits one separately-sorted array per page, never one sorted list. @@ -244,8 +238,10 @@ Close an item (WRITE, see the identity note above): gh issue close --repo / --comment "" --reason completed ``` -The `done` action closes with `--reason completed` (or `not planned` for `--not-planned`), the -values GitHub's issue-close accepts. `--reason "not planned"` needs the quoted space. +The `done` action closes with `--reason completed`, and with `--reason "not planned"` for +`--not-planned`. The accepted reasons are read at +[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). **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 @@ -274,12 +270,14 @@ gh issue close --repo / --comment "Duplicate of " --reason " ## Edit labels / assignees -Edit labels / assignees (WRITE, see the identity note above). Edits use -`--add-label`/`--remove-label` and `--add-assignee`/`--remove-assignee` (NOT `--label`, which -is `gh issue create` only): +Edit labels / assignees (WRITE, see the identity note above). This adapter edits labels and +assignees through `gh issue edit`; its flags are read at +[gh issue edit § Options](https://cli.github.com/manual/gh_issue_edit#options) (as of 2026-11-03; +recheck when that section renames the label or assignee flags): ```bash gh issue edit --repo / --add-label "" --remove-label "" +gh issue edit --repo / --remove-assignee "@me" # clear this session's own assignment ``` **Carve-out: claim assignment stays on the session identity.** The assignee MUST be the session @@ -293,12 +291,14 @@ above). They use `gh api` against `…/issues//assignees` instead, through th `wit_read_assignees` / `wit_add_assignee` / `wit_remove_assignee` / `wit_try_remove_assignee` helpers in `common.sh`. Those helpers take the same `read`/`write` writer argument as `wit_run_gh`, so the carve-out above is preserved: `claim` passes `read` (bare `gh`) and resolves -`@me` to the login explicitly, because REST takes a literal login rather than the `@me` alias. +`@me` to the login explicitly rather than passing the alias. -Note the two APIs differ on an unassignable user: `gh issue edit --add-assignee` failed loudly, -while REST `POST …/assignees` returns 201 and silently drops the login. `claim` therefore -re-reads the assignees and fails `4` (auth) when its own assignment did not land, rather than -reporting a held lease on an item the frontier still sees as unassigned. +`claim` then re-reads the assignees and fails `4` (auth) when its own assignment did not land, +rather than reporting a held lease on an item the frontier still sees as unassigned. What the +endpoint does with a login it cannot assign is read at +[assignees § Add assignees to an issue](https://docs.github.com/en/rest/issues/assignees#add-assignees-to-an-issue) +(as of 2026-11-03; recheck when that section changes its response for a login it does not +assign). Of the verbs beyond the lease protocol, `get-item`, `list-items` and `list-frontier` switch to REST under that restriction ("View item", "List items"). `list-sub-items`, `add-sub-item` and @@ -318,8 +318,9 @@ gh issue edit --repo / --body-file ## List labels -For `/work-items:track labels` and the label checks in `add` (bare read). `gh label list` -returns 30 labels unless `--limit` says otherwise: +For `/work-items:track labels` and the label checks in `add` (bare read). Always pass `--limit`; +the default cap is read at [gh label list § Options](https://cli.github.com/manual/gh_label_list#options) +(as of 2026-11-03; recheck when that section changes the `--limit` default): ```bash gh label list --repo / --limit 1000 --json name --jq '.[].name' | tr -d '\r' @@ -351,9 +352,10 @@ printf '%s\n\n%s\n' "Closes #" "$(cat /tmp/pr-body.md)" \ The prepended line is `change-link --repo /`'s `closes` text (CONTRACT.md "Change links"). -Match GitHub's issue-closing keyword set (`close`/`closes`/`closed`/`fix`/`fixes`/`fixed`/ -`resolve`/`resolves`/`resolved`) followed by `#`; the opt-out markers `Refs #` / -`No related issue:` leave the body untouched. +Match GitHub's issue-closing keyword set followed by `#`, read live at +[linking a pull request to an issue § Linking a pull request to an issue using a keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) +(as of 2026-11-03; recheck when that section changes the keyword list); the opt-out markers +`Refs #` / `No related issue:` leave the body untouched. ## Open linked PRs @@ -361,11 +363,12 @@ For `/work-items:work` selection, report whether item `` already has an open for closure, so a candidate whose work is in flight is dropped from the pickable frontier rather than re-picked. With the draft-aware reduction below, the same check serves `/work-items:work-loop`'s drain-exit evaluation. The authoritative signal is **GitHub's own computed close-linkage**, not a text -match over the PR body: the GraphQL `Issue.closedByPullRequestsReferences` connection returns -exactly the PRs GitHub links as closing this issue, the same linkage GitHub renders in the -issue sidebar and acts on for merge-time auto-close. Keep only the `OPEN`-state nodes whose -**head branch lives in the item's own repository**: a `MERGED` PR that closed the issue already -dropped it from the open frontier, a `CLOSED` (unmerged) PR is not in flight, and a PR from a fork +match over the PR body, read from the GraphQL `Issue.closedByPullRequestsReferences` connection. +What that linkage covers is read at +[linking a pull request to an issue § About linked issues and pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#about-linked-issues-and-pull-requests) +(as of 2026-11-03; recheck when that section changes which pull requests link to an issue). +Keep only the `OPEN`-state nodes whose **head branch lives in the item's own repository**: a +`MERGED` PR that closed the issue already dropped it from the open frontier, a `CLOSED` (unmerged) PR is not in flight, and a PR from a fork (or from another repository whose body says `Closes owner/repo#`) is an outsider's proposal, not our work in flight. Counting it would let anyone park an item for the whole stale-in-flight window by opening a PR. A fork that was deleted has a `null` head repository and is dropped too. @@ -426,10 +429,12 @@ cycle), never read the absent boolean as "no open PR". `-F n=` passes the num `Int` (typed); `-f` passes the owner/repo strings; the `tr -d '\r'` on the captured output follows the Windows/Git Bash rule under "Gotchas" (each page's boolean can otherwise arrive as `true\r`, which `grep -qx true` would then fail to match). -The `select(.state=="OPEN")` filter is **necessary, not redundant with `includeClosedPrs:false`**: -that argument suppresses only `CLOSED` (unmerged) PRs, so a `MERGED` PR still appears in the -connection and must be dropped here. Otherwise an issue whose only closing PR merged to a -non-default base (or that was reopened after a merge) would be wrongly reported as in-flight. +The `select(.state=="OPEN")` filter stays beside `includeClosedPrs:false`: the query output is +filtered to `OPEN` here rather than trusting the argument, so an issue whose only closing PR merged +(or that was reopened after a merge) is never reported in flight. What the argument filters is read +at the [`Issue` object reference](https://docs.github.com/en/graphql/reference/issues#issue)'s +`closedByPullRequestsReferences` field (as of 2026-11-03; recheck when that field's arguments +change). `first:100` requests the connection's maximum page (GitHub GraphQL caps `first`/`last` at 100). Because the connection retains `MERGED` nodes, this bound counts every PR the issue has *ever* linked as closing, not only the open ones, so a long merge/reopen history can push the @@ -445,21 +450,22 @@ lets the exit status be checked: in a bare pipeline `gh`'s non-zero exit is mask mid-pagination failure would reduce to a spurious `false`. Why GitHub's computed linkage instead of a body regex over `gh pr list --search`: -- **Fenced code blocks and HTML comments are inert for free.** GitHub does not link a closing - keyword that appears only inside a fenced code block or an HTML comment, so an example snippet - such as a fenced `Closes #` never surfaces here and never spuriously excludes the still-open - issue. There is no fence-tracking heuristic to maintain. The retired approach hand-rolled a +- **No fence-tracking heuristic to maintain.** Whether text in a PR body forms a link is + GitHub's call, so an example snippet such as a fenced `Closes #` is judged by GitHub, not by + a local parser. The retired approach hand-rolled a `jq` `gsub` that recognized only exactly-three backticks or tildes and silently missed four-or-more-backtick and indented fences. This closes the fence-blindness the prior regex carried. - **No word-boundary or number-boundary guards.** `#463` cannot collide with `#4630` / `#1463` and a keyword cannot match inside a longer word, because the reference is GitHub's parsed issue linkage, not a regex over raw text. -- **Base-branch correctness (behavior change).** GitHub forms the close-link only for a PR that - targets the repository's default branch. A closing keyword on any other base branch is ignored - and creates no linkage. This mechanic therefore does not exclude an issue whose only `Closes - #` lives on a non-default-base PR, whereas the retired raw-body regex counted it. That issue - now stays pickable, matching GitHub's real merge-time auto-close semantics. +- **Base-branch correctness (behavior change).** The retired raw-body regex counted a + `Closes #` on a PR against any base branch. This mechanic counts only what GitHub links, so + an issue whose only closing keyword sits on a PR GitHub does not link now stays pickable, + matching GitHub's merge-time auto-close. Which base branches link, and which text in a body is + ignored, are read at + [§ Linking a pull request to an issue using a keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) + (as of 2026-11-03; recheck when that section changes which base branch a keyword links on). - **Opt-out is intrinsic.** An intentional `Refs #` (reference without closing) never enters the closing linkage, so it correctly does not exclude its issue. That is the same opt-out the `pr-issue-linkage` gate honors, now with no keyword allow/deny list to keep in sync. @@ -512,12 +518,12 @@ caller, the same as a non-zero exit. The endpoint and its role mapping are cited ## Linked changes -For `/work-items:track changes`: the PRs GitHub links as closing item ``, in any state, from the -same `closedByPullRequestsReferences` connection as "Open linked PRs" above, so the same rules +For `/work-items:track changes`: the PRs linked as closing item ``, read from the same +`closedByPullRequestsReferences` connection as "Open linked PRs" above, so the same rules apply: `--paginate`, `-F` for the typed `Int`, the output captured and its exit status checked before any reduction, and `tr -d '\r'`. A failed query is not an empty result. Unlike the in-flight -check it keeps PRs from any head repository and asks for closed ones (`includeClosedPrs:true`), -because a caller deriving what shipped wants every `MERGED` node; the caller filters by `state`. +check it keeps PRs from any head repository and sets `includeClosedPrs:true`, because a caller +deriving what shipped wants every state; the caller filters by `state`. ``/`` are the item's own repository (bare read): ```bash @@ -616,22 +622,21 @@ reported, or a `gh` release changes the `run watch` or `pr checks --watch` inter ## Gotchas -Per-operation gotchas live with their sections above (the `--limit` truncation under "List -items", the `--add-label`-vs-`--label` rule under "Edit labels / assignees"). Cross-cutting: +Per-operation gotchas live with their sections above (the `--limit` rule under "List items"). +Cross-cutting: - **Windows `\r`.** Git Bash adds `\r` to `gh` output through `jq`/`--jq`; end every parsing pipeline with `| tr -d '\r'`. -- **`--json subIssues` nodes carry no `repository`.** Each node is projected down to `id`, - `number`, `title`, `url`, `state`. gh's own GraphQL query does ask for - `repository{nameWithOwner}`, but its export path drops the object again, and `parent`, - `blockedBy` and `blocking` project the same way (no `stateReason` either, so `get-item` and - `list-items` read closed blockers' close reasons with one `gh api graphql` `nodes(ids:)` - query). So filtering sub-issues on - `.repository.nameWithOwner` matches nothing and reads as "no children" (#3825). Derive - owner/repo from the node's `url` instead (`///issues/`), or request - `repository { nameWithOwner }` through `gh api graphql`, which does return it. Verified - against gh 2.97.0; the projection is unchanged from 2.94.0, the sub-issue floor, through - 2.98.0. A node the adapter can attribute to neither field is dropped and named on stderr, so +- **Never filter `--json subIssues` nodes on `.repository`.** Derive owner/repo from the node's + `url` (`///issues/`), or request `repository { nameWithOwner }` through + `gh api graphql`; a filter on `.repository.nameWithOwner` reads as "no children". `parent`, + `blockedBy` and `blocking` nodes get the same treatment, and `get-item` and `list-items` read + closed blockers' close reasons with one `gh api graphql` `nodes(ids:)` query. Which fields + `--json` exports is read at + [gh issue view § JSON Fields](https://cli.github.com/manual/gh_issue_view#json-fields) + (as of 2026-11-03; recheck when a gh release changes the sub-issue, parent or dependency + projection). + A node the adapter can attribute to neither field is dropped and named on stderr, so a further narrowing of the projection surfaces as a message instead of as another silently empty list. A node attributed to a *different* repo is dropped silently: that one is the documented cross-repo truncation, not a fault. @@ -657,5 +662,7 @@ items", the `--add-label`-vs-`--label` rule under "Edit labels / assignees"). Cr `-Encoding utf8` prepends a BOM and `utf8NoBOM` does not exist (PowerShell 6+ has both). Without `-Raw` you get a line array, not the one string that write takes. - **Rate limits**: see [API budget](#api-budget). -- **Issue Forms auto-labeling** fires only on web-form creation, not `gh issue create`, so apply - labels explicitly when creating programmatically. +- **Apply labels explicitly when creating an item programmatically**, never relying on an issue + form's `labels` key. Which creation paths apply that key is read at + [syntax for issue forms § Top-level syntax](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax) + (as of 2026-11-03; recheck when that section changes when the `labels` key applies).