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
2 changes: 1 addition & 1 deletion plugins/work-items/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
42 changes: 42 additions & 0 deletions plugins/work-items/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,48 @@
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 (`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
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

- **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`, `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

- **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
Expand Down
5 changes: 3 additions & 2 deletions plugins/work-items/reference/label-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 39 additions & 7 deletions plugins/work-items/reference/tracker-seam.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ TRACKER="${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/work-item-tracker.sh"
"$TRACKER" <verb>
```

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 (<https://jqlang.org/download/>; a separate install under Git
Expand All @@ -37,9 +40,35 @@ 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** (`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
`/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
Expand Down Expand Up @@ -99,7 +128,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
Expand Down
2 changes: 1 addition & 1 deletion plugins/work-items/skills/decompose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion plugins/work-items/skills/track/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions plugins/work-items/skills/track/actions/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions plugins/work-items/skills/track/actions/due.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down
4 changes: 2 additions & 2 deletions plugins/work-items/skills/track/actions/recheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down
2 changes: 2 additions & 0 deletions plugins/work-items/skills/triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #<carrier> 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:
Expand Down
19 changes: 17 additions & 2 deletions plugins/work-items/skills/work/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ derive `<slug>` 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".
Expand Down Expand Up @@ -63,8 +77,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.

Expand Down
Loading
Loading