From 08fe6e3e0bb2cd52e1c77eb0ad301bbe393c90be Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:25:54 -0400 Subject: [PATCH 1/3] feat(work-items): loud binding-presence entry invariant + role-default warning (#449) Add binding presence (.work-item-tracker.json) as a third loud entry invariant alongside jq / dispatcher-script, and convert the silent role-label-default fallback into a loud warning. Reconciles the ratified "stop-and-remediate" decision with this repo's own deliberately gh-native /work-items:work lane: a missing binding is discharged distinctly from jq/seam (which have no recovery path). Seam coordination verbs (claim, reclaim, renew-lease, create-item, list-frontier, capabilities) cannot run unbound, so before the first one the skill surfaces an actionable message distinguishing "setup never run" (run /work-items:setup) from a "deliberate gh-native operating mode" (proceed provider-mechanic-only, no race-safe claim/lease) instead of a raw mid-flow exit 3. Provider-mechanic operations (list/search/close, label/comment edits) run as raw gh and proceed unbound; their only degradation is role resolution, now a loud warning. local-markdown with no binding stays a hard stop. Full remote/no-checkout mode stays deferred. A first-class gh-native no-lease claim path for coordination-dependent lanes is parked for operator decision. Closes #449 Co-authored-by: Claude Opus 4.8 --- plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 37 +++++++++++++++ .../work-items/reference/label-taxonomy.md | 5 ++- plugins/work-items/reference/tracker-seam.md | 45 ++++++++++++++++--- plugins/work-items/skills/decompose/SKILL.md | 2 +- .../work-items/skills/track/actions/audit.md | 5 ++- .../work-items/skills/track/actions/due.md | 5 ++- .../skills/track/actions/recheck.md | 4 +- plugins/work-items/skills/work/SKILL.md | 5 ++- .../work-items/skills/work/evals/evals.json | 14 ++++++ 10 files changed, 105 insertions(+), 19 deletions(-) diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 4d26f59e0..9c0f42c18 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.14.4", + "version": "0.15.0", "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 0ca024274..95831476e 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,43 @@ 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.15.0] + +Close the work-items entry-invariant gap where a missing provider binding (`.work-item-tracker.json`) +degraded silently — role labels fell to defaults with no signal, and seam coordination verbs surfaced +a raw mid-flow `exit 3` instead of an actionable message (`#449`). The full remote / no-checkout mode +(shallow-clone or `gh api`-backed codebase reads) stays deferred with a recorded trigger. + +### Added + +- **Binding presence is a third loud entry invariant (`#449`).** "Shared tracker context" now checks + the provider binding alongside `jq` and the seam script, but discharges it distinctly: the first two + have no recovery path and stop; a missing binding is loud and routable, never a silent default and + never a raw `exit 3`. Seam **coordination** verbs (`claim`, `reclaim`, `renew-lease`, `create-item`, + `list-frontier`, `capabilities`) cannot run unbound, so before the first one the skill surfaces a + message distinguishing **setup was never run** (→ `/work-items:setup`) from a **deliberate gh-native + operating mode** (proceed for provider-mechanic operations only, accepting no race-safe claim/lease). + Provider-mechanic operations (list/search/close, label/comment edits) run as raw `gh`, never read + the binding, and proceed unbound. Caveat recorded: the gh-native path presumes a `gh`-backed + provider — a `local-markdown` target with no binding cannot proceed and stays a hard stop. + +### Changed + +- **Silent role-label default becomes a loud warning (`#449`).** When a canonical role resolves to its + documented default because `.work-item-tracker.json` or its `config.role_labels` entry is absent, the + skills now warn loudly instead of substituting silently — a repo that remapped `config.role_labels` + was previously queried under the wrong strings with no signal. Applied at every action-entry + resolution site that inlines it (`work`, `track` `due`/`recheck`/`audit`, `decompose`) and in the + shared invariants (`reference/tracker-seam.md`, `reference/label-taxonomy.md`). A present-but-malformed, + empty, or non-string configured value remains a hard stop, unchanged. + +### Deferred + +- **A first-class gh-native no-lease claim path for coordination-*dependent* lanes (`/work-items:work`) + is parked, not built (`#449`).** Making those lanes runnable unbound (assignee-only claim, no lease, + races are the operator's problem) is claim-safety contract surface — deferred with the same trigger + as the full remote-repo mode: someone needs unattended coordination-dependent work at scale. + ## [0.14.4] ### Fixed diff --git a/plugins/work-items/reference/label-taxonomy.md b/plugins/work-items/reference/label-taxonomy.md index 8a99ececf..b918ac6d0 100644 --- a/plugins/work-items/reference/label-taxonomy.md +++ b/plugins/work-items/reference/label-taxonomy.md @@ -51,8 +51,9 @@ Binding shape (every entry optional; unlisted roles keep their defaults): Resolve the mapping at the start of every action invocation that uses a role and use the resolved strings in every provider query and core-side label comparison for that invocation. Do not cache a mapping across invocations: the tracked binding may change between them. An absent file or absent -entry uses the default; a present malformed, empty, or non-string entry is a configuration error, -not permission to fall back silently. Two constraints on remapping: +entry falls back to the default **with a loud warning** (surface it — silent defaulting hides a remap +mismatch); a present malformed, empty, or non-string entry is a configuration error, not permission to +fall back silently. Two constraints on remapping: - **`human-gated` is shared with the seam.** `list-frontier --autonomous` excludes items by that label, and the shipped seam reads `needs-human`; remap this role only when the bound seam diff --git a/plugins/work-items/reference/tracker-seam.md b/plugins/work-items/reference/tracker-seam.md index 8e9136ba8..5fadf7173 100644 --- a/plugins/work-items/reference/tracker-seam.md +++ b/plugins/work-items/reference/tracker-seam.md @@ -25,9 +25,12 @@ TRACKER="${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/work-item-tracker.sh" "$TRACKER" ``` -The executable snippets in each action resolve `"$TRACKER"` that way, then invoke it. Two entry-point -presence checks are required for correctness before an invocation's first verb, each stopping with its -remediation rather than failing mid-action: +The executable snippets in each action resolve `"$TRACKER"` that way, then invoke it. Three entry +invariants are checked before an invocation's first verb. The first two — `jq` and the seam script — +have **no recovery path**, so each stops with its remediation rather than failing mid-action. The +third — the provider **binding** — is discharged differently: a missing binding blocks only seam +**coordination** verbs, never the provider-mechanic operations that run as raw `gh`, so it warns and +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 @@ -37,9 +40,34 @@ remediation rather than failing mid-action: plugin install is incomplete — stop and surface that the plugin must be reinstalled or repaired, rather than improvising provider commands. `/work-items:setup` binds the provider and configures the recurring schedule and label remaps but does NOT create the seam. - -The repo's active provider is bound in `.work-item-tracker.json` at the project root — the setup skill -seeds it. Adapters resolve the opposite way — **consumer-local-first, plugin-bundled fallback** +- **The provider binding** (`.work-item-tracker.json` at the project root — `/work-items:setup` seeds + it). Unlike the two above, a missing binding has a legitimate recovery path, so it is **loud and + actionable, never a silent default and never a raw mid-flow `exit 3`** — but it does not halt the + invocation unconditionally: + - **Seam coordination verbs** (`claim`, `reclaim`, `renew-lease`, `create-item`, `list-frontier`, + `capabilities`) cannot run without a binding — the seam hard-errors `exit 3` + (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` "Exit codes"). Before the first + coordination verb, if no binding resolves, surface a message that distinguishes the two ways to + arrive here rather than dead-ending on the raw `exit 3`: **(1) setup was never run** → run + `/work-items:setup` to bind the provider; **(2) a deliberate gh-native operating mode** → the lane + may proceed for provider-mechanic operations only, accepting that **no race-safe claim/lease is + available** — the seam coordination verbs stay unavailable and claim collisions become the + operator's responsibility. + - **Provider-mechanic operations** (list/search/aggregate, close, label/assignee/comment edits) run + as raw `gh` per the bound adapter's operations reference and never read the binding, so they + proceed unbound. Their only degradation is canonical-role resolution, which falls to defaults + **with a loud warning** ("Role-label resolution is an action-entry invariant" below). + - **Caveat — the gh-native path presumes a `gh`-backed provider.** A `local-markdown` target with no + binding has no `config.storage_dir` and cannot proceed at all; there a missing binding is a hard + stop, not a gh-native fallback. + + Formally documenting a first-class gh-native **claim** path (assignee-only, no lease) for + coordination-*dependent* lanes such as `/work-items:work` — so they too can run unbound instead of + stopping at the coordination check — is a separate decision deferred with the same trigger as the + full remote-repo mode (someone needs unattended coordination-dependent work at scale). This + invariant's job is only to make a missing binding loud and routable, never silent. + +Adapters resolve the opposite way — **consumer-local-first, plugin-bundled fallback** (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` "Adapter resolution") — so a repo can add an unshipped provider or shadow a bundled one without forking the plugin. Coordination — create, claim (assignee + lease), lease renew/reclaim, dependency links, sub-items, frontier selection, single-item @@ -99,7 +127,10 @@ from the tracker binding's `config.role_labels` (defaults `agent-ready` / `needs At the start of every action that queries, creates, or filters items by a canonical role, read `.work-item-tracker.json` and resolve each role the action uses from `config.role_labels`; an absent -file or absent entry uses the documented default. Keep those resolved strings for that invocation and +file or absent entry falls back to the documented default **with a loud warning** — surface that the +binding (or that role's entry) is absent and the role is running under its default, a real correctness +risk when the consuming repo remapped `config.role_labels`, rather than substituting silently. Keep +those resolved strings for that invocation and use them in every adapter query and core-side label comparison. Never put a default literal such as `recurring` into a provider query after the role has been remapped. A present binding with invalid JSON, a non-string role value, or an empty role value is a configuration error: stop and report it diff --git a/plugins/work-items/skills/decompose/SKILL.md b/plugins/work-items/skills/decompose/SKILL.md index faa213411..aad3691a1 100644 --- a/plugins/work-items/skills/decompose/SKILL.md +++ b/plugins/work-items/skills/decompose/SKILL.md @@ -61,7 +61,7 @@ Break into **tracer-bullet** items. Each item is a thin vertical slice cutting t | **AFK** | Implementable and mergeable without human interaction | autonomous-eligible (default `agent-ready`) | | **HITL** | Requires human decision, design review, or manual testing | human-gated (default `needs-human`) | -Prefer AFK. Mark HITL only when the slice genuinely needs judgment (architectural decision, UX review, external-system access, manual QA). Both are canonical roles — resolve each repo-actual label string from the binding's `config.role_labels`, defaulting to the strings shown ([`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Canonical roles"). +Prefer AFK. Mark HITL only when the slice genuinely needs judgment (architectural decision, UX review, external-system access, manual QA). Both are canonical roles — resolve each repo-actual label string from the binding's `config.role_labels`, defaulting to the strings shown — and warn loudly when a role defaults because the binding or its `config.role_labels` entry is absent, rather than substituting silently ([`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Canonical roles"). The human-gated label (default `needs-human`) is what keeps a slice out of autonomous pickup — `list-frontier --autonomous` excludes it (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` "Verbs (core public surface)"). Merely omitting the autonomous-eligible label does NOT: the frontier filter keys on the human-gated label, not on the absence of the other, so an unlabeled HITL slice would still be claimable by `/work-items:work`. The autonomous-eligible label (default `agent-ready`) is the positive autonomous-pickup eligibility marker; the two labels gate different filters and an HITL slice wants the human-gated label set AND the autonomous-eligible one omitted. diff --git a/plugins/work-items/skills/track/actions/audit.md b/plugins/work-items/skills/track/actions/audit.md index e90fffa56..8dda07334 100644 --- a/plugins/work-items/skills/track/actions/audit.md +++ b/plugins/work-items/skills/track/actions/audit.md @@ -11,8 +11,9 @@ Detect stale claims, orphaned recurring entries, and label hygiene issues. ## Checks Before any tracker read, resolve `recurring-maintenance` from `.work-item-tracker.json` -`config.role_labels`, using `recurring` only when the file or entry is absent. Stop on a malformed, -empty, or non-string configured value. Keep the resolved string for every recurring-item query and +`config.role_labels`, using `recurring` only when the file or entry is absent — and warn loudly when +it defaults for that reason (surface it, never silent). Stop on a malformed, empty, or non-string +configured value. Keep the resolved string for every recurring-item query and comparison in this audit. ### 1. Stale claims diff --git a/plugins/work-items/skills/track/actions/due.md b/plugins/work-items/skills/track/actions/due.md index fdc535538..8e95e48ef 100644 --- a/plugins/work-items/skills/track/actions/due.md +++ b/plugins/work-items/skills/track/actions/due.md @@ -13,8 +13,9 @@ Show recurring items that are past their `next_due` date. 1. **Resolve the recurring-maintenance role label before any tracker read.** Read `.work-item-tracker.json` at action entry and resolve `config.role_labels["recurring-maintenance"]`; use `recurring` only when the file or entry is - absent. Stop on a malformed, empty, or non-string configured value. Use the resolved string in - every adapter filter below. + absent — and warn loudly when it defaults for that reason (surface it, never silent). Stop on a + malformed, empty, or non-string configured value. Use the resolved string in every adapter filter + below. 1. **Read the recurring schedule:** diff --git a/plugins/work-items/skills/track/actions/recheck.md b/plugins/work-items/skills/track/actions/recheck.md index 252d02b1c..da706b0c3 100644 --- a/plugins/work-items/skills/track/actions/recheck.md +++ b/plugins/work-items/skills/track/actions/recheck.md @@ -13,8 +13,8 @@ Update a recurring item's `last_checked` and `next_due` dates after completing a 1. **Resolve the recurring-maintenance role label before any tracker read.** Read `.work-item-tracker.json` at action entry and resolve `config.role_labels["recurring-maintenance"]`; use `recurring` only when the file or entry is - absent. Stop on a malformed, empty, or non-string configured value. Use the resolved string in - the search below. + absent — and warn loudly when it defaults for that reason (surface it, never silent). Stop on a + malformed, empty, or non-string configured value. Use the resolved string in the search below. 1. **Find the item in the recurring schedule:** diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 1c7ede725..27dddf2e5 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -63,8 +63,9 @@ Exit `6` (capability-unsupported, CONTRACT.md "Exit codes") means the bound prov ### Role-label preflight Before any tracker read, resolve `recurring-maintenance` from `.work-item-tracker.json` -`config.role_labels`, using `recurring` only when the file or entry is absent. Stop on a malformed, -empty, or non-string configured value. Use the resolved string for every recurring/non-recurring +`config.role_labels`, using `recurring` only when the file or entry is absent — and warn loudly when +it defaults for that reason (surface it, never silent). Stop on a malformed, empty, or non-string +configured value. Use the resolved string for every recurring/non-recurring filter and every adapter query in this action; do not compare labels against the default literal after a remap. diff --git a/plugins/work-items/skills/work/evals/evals.json b/plugins/work-items/skills/work/evals/evals.json index 63c4da38a..67ea58ec3 100644 --- a/plugins/work-items/skills/work/evals/evals.json +++ b/plugins/work-items/skills/work/evals/evals.json @@ -45,6 +45,20 @@ "Does not resolve the VALID-but-deferred finding without a follow-up issue filed via /work-items:track add and cited in both the reply and `## Related`", "Hands the PR off to /source-control:babysit-prs and never merges — work's lane ends at PR creation plus handoff" ] + }, + { + "id": 4, + "name": "work-unbound-binding-is-loud-and-routable-not-raw-exit-3", + "prompt": "/work-items:work\n\nThere is a full local checkout (git top-level resolves) but no .work-item-tracker.json anywhere in the repo — /work-items:setup was never run here.", + "expected_output": "Before the first seam coordination verb (the Step 0 session-start reclaim), the skill detects the missing binding and surfaces a clear, actionable message rather than dead-ending on the raw seam exit 3. The message distinguishes the two ways to arrive here: setup was never run (remediate by running /work-items:setup to bind the provider) versus a deliberate gh-native operating mode (proceed for provider-mechanic operations only — list/search/comment/label — accepting that no race-safe claim/lease is available and coordination verbs stay unavailable). It never silently substitutes role-label defaults: when a canonical role resolves to its default because the binding is absent, it warns loudly.", + "files": [], + "expectations": [ + "Detects the missing `.work-item-tracker.json` binding before the first coordination verb rather than surfacing a raw seam `exit 3` mid-flow", + "Surfaces an actionable message distinguishing `setup was never run` (→ run `/work-items:setup`) from a deliberate gh-native operating mode", + "Names the gh-native tradeoff: provider-mechanic operations proceed unbound, but no race-safe claim/lease is available and seam coordination verbs stay unavailable", + "When a canonical role resolves to its default because the binding is absent, warns loudly instead of substituting the default silently", + "Does not fabricate or forge a binding to proceed — surfaces the choice instead" + ] } ] } From 31a4a7ba926b41bb5111f5b974f0f59ac487c8d3 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:37:36 -0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(work-items):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20bind=20preflight=20before=20Step=200,=20triage/trac?= =?UTF-8?q?k=20role-warning=20parity=20(#449)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings (PR #690): - F1 (medium): work Step 0 reclaim is the lane's first coordination verb but ran structurally before the binding was read, so the invariant surfaced as a raw exit 3. Add an explicit binding preflight before Step 0 that discharges the setup-vs-gh-native routing (and stops rather than dispatching claimless, since the gh-native no-lease claim path for this coordination-dependent lane is parked). - F2 (low): triage applied canonical role labels without an inline resolution/ loud-warning reminder; add one mirroring decompose (reference layer already covered it — this restores site-level consistency). - F3 (low): track/SKILL.md parent role-invariant summary now carries the loud-warning clause its children (due/recheck/audit) already gained. CHANGELOG updated to reflect the added sites. Co-authored-by: Claude Opus 4.8 --- plugins/work-items/CHANGELOG.md | 10 +++++++--- plugins/work-items/skills/track/SKILL.md | 3 ++- plugins/work-items/skills/triage/SKILL.md | 2 ++ plugins/work-items/skills/work/SKILL.md | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 95831476e..2eaaffe4d 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -22,6 +22,9 @@ a raw mid-flow `exit 3` instead of an actionable message (`#449`). The full remo Provider-mechanic operations (list/search/close, label/comment edits) run as raw `gh`, never read the binding, and proceed unbound. Caveat recorded: the gh-native path presumes a `gh`-backed provider — a `local-markdown` target with no binding cannot proceed and stays a hard stop. + `/work-items:work` gains an explicit binding preflight **before Step 0** — its `reclaim` is the + lane's first coordination verb, so the check is discharged before it runs rather than surfacing as a + raw mid-reclaim `exit 3`. ### Changed @@ -29,9 +32,10 @@ a raw mid-flow `exit 3` instead of an actionable message (`#449`). The full remo documented default because `.work-item-tracker.json` or its `config.role_labels` entry is absent, the skills now warn loudly instead of substituting silently — a repo that remapped `config.role_labels` was previously queried under the wrong strings with no signal. Applied at every action-entry - resolution site that inlines it (`work`, `track` `due`/`recheck`/`audit`, `decompose`) and in the - shared invariants (`reference/tracker-seam.md`, `reference/label-taxonomy.md`). A present-but-malformed, - empty, or non-string configured value remains a hard stop, unchanged. + resolution site that inlines it (`work`, `triage`, `track` — `SKILL.md` summary plus + `due`/`recheck`/`audit` — and `decompose`) and in the shared invariants (`reference/tracker-seam.md`, + `reference/label-taxonomy.md`). A present-but-malformed, empty, or non-string configured value + remains a hard stop, unchanged. ### Deferred diff --git a/plugins/work-items/skills/track/SKILL.md b/plugins/work-items/skills/track/SKILL.md index 65b92336f..37989f282 100644 --- a/plugins/work-items/skills/track/SKILL.md +++ b/plugins/work-items/skills/track/SKILL.md @@ -25,7 +25,8 @@ actions below in particular: - **Role-label resolution is an action-entry invariant.** `add`, `due`, `recheck`, and `audit` query, create, or filter items by a canonical role — resolve each role from `.work-item-tracker.json` `config.role_labels` at action entry and use the resolved strings in - every query. + every query. When a role defaults because the file or entry is absent, warn loudly rather than + substituting silently; a present malformed/empty/non-string value is a hard stop. ## Scope diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index 07222bb28..62ff1cb38 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -135,6 +135,8 @@ For a PR, the outcome addresses the attached code explicitly: adopt the diff (br **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. +The canonical-role labels applied by these outcomes (autonomous-eligible default `agent-ready`, human-gated default `needs-human`) are **resolved from the binding's `config.role_labels` at action entry**, never hardcoded — warn loudly when a role defaults because `.work-item-tracker.json` or the entry is absent rather than applying the default string silently (a repo that remapped roles would otherwise be mislabeled), and stop on a malformed/empty/non-string value ([`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Canonical roles"). + Label edits, comments, and closes route through the adapter's write mechanics (adapter: "Edit labels / assignees", "Comment on item / edit a comment", "Close item"); the gather + attention-view reads are bare. Item creation, when triage spawns follow-up work, goes through the seam `create-item` verb (`/work-items:track add` is the canonical path). **Closing invariant — no outcome leaves a re-selectable raw item.** The attention view lists *open* items and re-selects anything still carrying the raw marker, so every outcome must leave the item unre-selectable: diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 27dddf2e5..e2b4cb8ad 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -34,6 +34,20 @@ derive `` per its slug spec and, on the session's first memory-tier write, memory root's self-ignore guard (a `.gitignore` containing `*`, created and announced when absent). Tick each step as completed. +## Binding preflight (before Step 0) + +Step 0's `reclaim` is this lane's **first seam coordination verb**, so the binding-presence entry +invariant ([`${CLAUDE_PLUGIN_ROOT}/reference/tracker-seam.md`](${CLAUDE_PLUGIN_ROOT}/reference/tracker-seam.md) +"Shared tracker context") is discharged **here, before Step 0 runs** — never left to surface as a raw +`exit 3` mid-reclaim. If `.work-item-tracker.json` does not resolve, surface the actionable choice +before attempting `reclaim`: **(1) setup was never run** → run `/work-items:setup` to bind the +provider; **(2) a deliberate gh-native operating mode** → this lane is coordination-*dependent* (Step +0 `reclaim`, `list-frontier`, and the Step 5 `claim` are all seam verbs that need the binding), so an +unbound run cannot acquire a race-safe claim/lease. Do NOT silently skip the claim and dispatch anyway +(claim-before-dispatch is a Step 5 invariant): surface that the lane is unbound and stop for the +remediation. A first-class gh-native no-lease claim path for this lane is a parked decision, not yet a +supported mode. A `local-markdown` target with no binding cannot proceed at all. + ## Step 0: Session-start reclaim (idempotent) Before selecting, clear stale claims left by crashed or abandoned sessions (an idempotent entry step). Enumerate currently-assigned items (adapter: "List items", assigned filter — the rows carry `number`), resolve each `number` to a fully-qualified id (adapter: "Resolve item ID"; `reclaim` rejects a bare number), and run the seam `reclaim` verb on each id — idempotent; outcome + activity-check semantics per `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` "Lease protocol". From 2c16cd008217377fc97d21888c69ba39e8ba5acf Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:33:50 -0400 Subject: [PATCH 3/3] fix(work-items): include single-item fetch and dependency-link verbs in the binding preflight (#449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seam coordination-verb list in tracker-seam.md's binding-preflight section omitted get-item, link-blocks, and add-sub-item — all three already documented elsewhere in the same file as coordination verbs that go through the seam ("single-item fetch", "dependency links", "sub-items"), and all three require a binding at the dispatcher unconditionally (work-item-tracker.sh:98-102), before its verb-specific dispatch. An unbound /work-items:track start calls get-item before reclaim (start.md:20-21), so it still hit the raw exit 3 this invariant was meant to prevent. CHANGELOG's restatement of the same list is updated to match. Co-authored-by: Claude Opus 4.8 --- plugins/work-items/CHANGELOG.md | 7 ++++--- plugins/work-items/reference/tracker-seam.md | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 2eaaffe4d..279a704d2 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -15,9 +15,10 @@ a raw mid-flow `exit 3` instead of an actionable message (`#449`). The full remo - **Binding presence is a third loud entry invariant (`#449`).** "Shared tracker context" now checks the provider binding alongside `jq` and the seam script, but discharges it distinctly: the first two have no recovery path and stop; a missing binding is loud and routable, never a silent default and - never a raw `exit 3`. Seam **coordination** verbs (`claim`, `reclaim`, `renew-lease`, `create-item`, - `list-frontier`, `capabilities`) cannot run unbound, so before the first one the skill surfaces a - message distinguishing **setup was never run** (→ `/work-items:setup`) from a **deliberate gh-native + never a raw `exit 3`. Seam **coordination** verbs (`create-item`, `get-item`, `claim`, `renew-lease`, + `reclaim`, `link-blocks`, `add-sub-item`, `list-frontier`, `capabilities`) cannot run unbound, so + before the first one the skill surfaces a message distinguishing **setup was never run** (→ + `/work-items:setup`) from a **deliberate gh-native operating mode** (proceed for provider-mechanic operations only, accepting no race-safe claim/lease). Provider-mechanic operations (list/search/close, label/comment edits) run as raw `gh`, never read the binding, and proceed unbound. Caveat recorded: the gh-native path presumes a `gh`-backed diff --git a/plugins/work-items/reference/tracker-seam.md b/plugins/work-items/reference/tracker-seam.md index 5fadf7173..d9382a32e 100644 --- a/plugins/work-items/reference/tracker-seam.md +++ b/plugins/work-items/reference/tracker-seam.md @@ -44,8 +44,9 @@ routes rather than halting unconditionally. it). Unlike the two above, a missing binding has a legitimate recovery path, so it is **loud and actionable, never a silent default and never a raw mid-flow `exit 3`** — but it does not halt the invocation unconditionally: - - **Seam coordination verbs** (`claim`, `reclaim`, `renew-lease`, `create-item`, `list-frontier`, - `capabilities`) cannot run without a binding — the seam hard-errors `exit 3` + - **Seam coordination verbs** (`create-item`, `get-item`, `claim`, `renew-lease`, `reclaim`, + `link-blocks`, `add-sub-item`, `list-frontier`, `capabilities`) cannot run without a binding — + the seam hard-errors `exit 3` (`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md` "Exit codes"). Before the first coordination verb, if no binding resolves, surface a message that distinguishes the two ways to arrive here rather than dead-ending on the raw `exit 3`: **(1) setup was never run** → run