diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index d403eff7d..825775f37 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.35.7", + "version": "0.35.8", "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, raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. 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", @@ -22,12 +22,12 @@ "lane_instance": { "type": "string", "title": "Lane instance id", - "description": "Writer identity for this machine's loop-lane telemetry, per the loop-lane convention's lane-instance identity rule. It becomes the suffix of the lane's telemetry sentinel marker (`work-items:work-loop@`), so each concurrently running lane instance owns its own comment and none can overwrite another's durable state — including first_drain_complete, whose loss would end one machine's earn-trust ratification gate because a different machine finished a drain. Must match ^[a-z0-9][a-z0-9-]{0,31}$, be stable across restarts, and be distinct across concurrent instances; two lanes on one machine each need an explicit value. Absent: the sanitized lowercased hostname. The value appears verbatim in tracker comments — set an opaque id if a machine name should not be published in a public tracker." + "description": "Writer identity for this machine's loop-lane telemetry, per the loop-lane convention's lane-instance identity rule. It becomes the suffix of the lane's telemetry sentinel marker (`work-items:work-loop@`), so each concurrently running lane instance owns its own comment and none can overwrite another's durable state \u2014 including first_drain_complete, whose loss would end one machine's earn-trust ratification gate because a different machine finished a drain. Must match ^[a-z0-9][a-z0-9-]{0,31}$, be stable across restarts, and be distinct across concurrent instances; two lanes on one machine each need an explicit value. Absent: the sanitized lowercased hostname. The value appears verbatim in tracker comments \u2014 set an opaque id if a machine name should not be published in a public tracker." }, "work_dispatch_concurrency_cap": { "type": "number", "title": "Autonomous dispatch concurrency cap", - "description": "Maximum concurrent dispatch waves /work-items:work's autonomous execute step allows per invocation (it runs exactly one item per invocation). Give a whole number of waves; a fractional value is floored to whole waves since a wave is discrete. When set, /work-items:work threads it into /implementation:implement-dispatch as that skill's --wave-cap ceiling. Leave unset to let implement-dispatch apply its own internal 3-5 wave default — this key declares no default, so an unset value stays distinguishable from a configured one (which a declared default would collapse into a hard cap).", + "description": "Maximum concurrent dispatch waves /work-items:work's autonomous execute step allows per invocation (it runs exactly one item per invocation). Give a whole number of waves; a fractional value is floored to whole waves since a wave is discrete. When set, /work-items:work threads it into /implementation:implement-dispatch as that skill's --wave-cap ceiling. Leave unset to let implement-dispatch apply its own internal 3-5 wave default \u2014 this key declares no default, so an unset value stays distinguishable from a configured one (which a declared default would collapse into a hard cap).", "min": 1 }, "work_loop_item_cap_start": { @@ -54,7 +54,7 @@ "work_loop_frontier_item_cap_ceiling": { "type": "number", "title": "Work-loop frontier-tier item cap ceiling", - "description": "Quota guard for frontier-capability-tier items in the work-loop lane: such items run at concurrency 1 and their adaptive cap is bounded by this ceiling instead of the general one. Keep it at or below work_loop_item_cap_ceiling. The frontier tier is read from the item body, which any item author can write, so a frontier ceiling above the general one would let a body claim buy higher throughput; the lane detects that inversion and ignores this ceiling, bounding the item by the general one instead. The manifest cannot enforce the ordering — userConfig min/max are static bounds with no cross-key validation.", + "description": "Quota guard for frontier-capability-tier items in the work-loop lane: such items run at concurrency 1 and their adaptive cap is bounded by this ceiling instead of the general one. Keep it at or below work_loop_item_cap_ceiling. The frontier tier is read from the item body, which any item author can write, so a frontier ceiling above the general one would let a body claim buy higher throughput; the lane detects that inversion and ignores this ceiling, bounding the item by the general one instead. The manifest cannot enforce the ordering \u2014 userConfig min/max are static bounds with no cross-key validation.", "default": 2, "min": 1 }, diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index a765a17d5..7940fe93f 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,12 @@ 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.35.8] + +### Fixed + +- Harden triage intake against priority-axis stacking and blocked-by relabel races. + ## [0.35.7] ### Added diff --git a/plugins/work-items/reference/issue-conventions.md b/plugins/work-items/reference/issue-conventions.md index cd963aceb..787560561 100644 --- a/plugins/work-items/reference/issue-conventions.md +++ b/plugins/work-items/reference/issue-conventions.md @@ -17,6 +17,11 @@ dialects are accepted: Umbrella / epic items use an `Epic:` prefix or a trailing `(umbrella)`. A child-of relationship is recorded as a native sub-issue edge, **never** as a title suffix. +When folding a member into an umbrella, treat the sequence as **one atomic action**: post the +membership comment on the item, post the matching membership comment on the umbrella issue, record +the `blocked-by` edge, then strip the raw marker — never stop after the item-side comment alone +(#633). + Recurring maintenance items keep the `[Maintenance] {title}` shape owned by `track add --recurring` (the due / work / recheck flows exact-match that prefix); they are exempt from the two prefix dialects. diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index 6d94286dc..babf89598 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -88,7 +88,7 @@ Show three buckets (oldest first, one-line summaries): 2. **Raw marker** — `status:needs-triage` / `priority:needs-triage`, whichever axis the repo files it under — explicitly tagged for evaluation 3. **`status:needs-info` with reporter activity** — reporter replied since last triage note; ready for re-evaluation -List open items and filter into buckets programmatically (adapter: "List items", bare read). Apply the lane-infrastructure exclusion ("Scope: raw intake only") to that listing **before** bucketing, so a telemetry issue carrying the raw marker is filtered out rather than bucketed under it. When the repo treats external PRs as a request surface, include them and tag each line `[PR]` or `[issue]` — but surface only *external* PRs (a collaborator's in-flight PR is not triage work; this filter is discovery-only, and an explicitly named PR is always triaged regardless of author). Present as a compact table. +List open items and filter into buckets programmatically (adapter: "List items", bare read). Apply the lane-infrastructure exclusion ("Scope: raw intake only") to that listing **before** bucketing, so a telemetry issue carrying the raw marker is filtered out rather than bucketed under it. **Defensive skip:** drop any item that already carries a native `blocked-by` edge *and* a prior triage comment (machine disclaimer or structured needs-info template) — a stray re-label from another lane must not cost a full re-investigation (#646). When the repo treats external PRs as a request surface, include them and tag each line `[PR]` or `[issue]` — but surface only *external* PRs (a collaborator's in-flight PR is not triage work; this filter is discovery-only, and an explicitly named PR is always triaged regardless of author). Present as a compact table. ## Triage workflow (with number) @@ -105,7 +105,7 @@ Read the item body, comments, and any linked PRs; for a PR, the diff too (adapte Classify **bug vs enhancement** first — it steers the rest of the flow (bugs get reproduced; rejected enhancements get ledgered). Then recommend: - **Type** — bug → `Bug`; enhancement → `Feature` (or `Task` for tracked non-feature work). Native GitHub Issue Type on org repos, set through the seam; `type:` label on personal / non-org repos. Item title and prefix conventions: [`${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md`](${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md) -- **Priority label** — resolve the live `priority:` label set from the bound adapter at action entry (for the GitHub adapter, `gh label list --search 'priority:'`; members are never snapshotted here — [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Universal axes"). Default to the resolved set's **assessed-default** tier — the mid-urgency member when the set follows the conventional critical/high/medium/low ordering (e.g. `priority: medium`, if present) — when no directive, category rule, or severity signal sets one. Reserve the next tier up (e.g. `priority: high`) for items that block other work or carry an imminent external deadline; the top tier (e.g. `priority: critical`) keeps its existing critical semantics. A live set that doesn't follow that ordering has no default to infer by convention — ask, or omit the label the way a repo-undefined default is omitted elsewhere ([`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md) "Priority"). When a directive or category rule sets this label **above** the finding's self-labeled severity, record the original severity in the triage comment (e.g. `priority set to by ; reporter severity: `) so implementers can sub-sort within a priority band. No new labels. This triage-assessed default is deliberately distinct from the `/work-items:track add` filing default (the resolved set's lowest-urgency tier, an untriaged-signal floor rather than a priority assessment) +- **Priority label** — resolve the live `priority:` label set from the bound adapter at action entry (for the GitHub adapter, `gh label list --search 'priority:'`; members are never snapshotted here — [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) "Universal axes"). The `priority:` axis is **single-valued**: before applying the assessed priority in step 5, remove every other `priority:` label already on the item (same replace-not-stack rule used when clearing the raw marker). Default to the resolved set's **assessed-default** tier — the mid-urgency member when the set follows the conventional critical/high/medium/low ordering (e.g. `priority: medium`, if present) — when no directive, category rule, or severity signal sets one. Reserve the next tier up (e.g. `priority: high`) for items that block other work or carry an imminent external deadline; the top tier (e.g. `priority: critical`) keeps its existing critical semantics. A live set that doesn't follow that ordering has no default to infer by convention — ask, or omit the label the way a repo-undefined default is omitted elsewhere ([`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md) "Priority"). When a directive or category rule sets this label **above** the finding's self-labeled severity, record the original severity in the triage comment (e.g. `priority set to by ; reporter severity: `) so implementers can sub-sort within a priority band. No new labels. This triage-assessed default is deliberately distinct from the `/work-items:track add` filing default (the resolved set's lowest-urgency tier, an untriaged-signal floor rather than a priority assessment) - **Target state** — from the state machine above: needs-info, or one of the three briefed exits (delegable, decision-defaulted, human-gated). For a briefed item that carries a decision, apply the **routing test**: is the alternative reversible/maintainer-vetoable (→ **decision-defaulted**: autonomous-eligible role + `status:ready`, recorded with a `Decision defaulted: X — veto before merge` comment) or genuinely open — open design space, product intent, or cross-repo policy (→ **human-gated**)? **Direction gate.** Recommending is read-only; the gate governs *mutation* — labels, comments, closes, item creation — and which side of it you are on is fixed by how triage was invoked: