diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 588a1b1576..ce43c51864 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.19", + "version": "0.35.21", "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", @@ -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 \u2014 userConfig min/max are static bounds with no cross-key validation.", + "description": "Quota guard for frontier-capability-tier items in the work-loop lane: items carrying capability-tier: frontier 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 provider-permissioned label only; absent label = general tier (fail-closed).", "default": 2, "min": 1 }, diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 80cd8e179d..6cdae9be28 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,28 @@ 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.21] + +### Fixed + +- **`/work-items:setup` provisions and backfills `capability-tier: frontier` (#1716 review).** + `check` probe 8 FAILs when the canonical member is absent; `apply` step 4 provisions it (same + mechanics as the work-class axis); step 5 backfills open items carrying legacy triage-briefing + body stamps via `scripts/backfill-capability-tier-labels.sh` — load-bearing because triage refuses + to re-triage already-triaged output. Legacy pattern detection lives in + `scripts/lib/legacy-frontier-tier-signal.sh`. + +## [0.35.20] + +### Fixed + +- **`work-loop` frontier-tier signal is the `capability-tier: frontier` label (#1716).** The + adaptive-cap quota guard no longer reads a triage-briefing body claim. Missing label fails + closed to the general tier; body prose is context only. Carve-out instance removed from + `item-content-trust.md`; taxonomy, `capability-tier-labels.md`, tracker-seam, triage stamp, + and manifest/README descriptions updated. Label provisioning for this repo requires + `melodic-software/github-iac` — the reader lands fail-closed until the label exists. + ## [0.35.19] ### Fixed diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index f91f30057a..eeb0340dee 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -157,7 +157,7 @@ reads it from. | `work_loop_item_cap_start` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_START` | Where the work-loop lane's adaptive per-cycle item cap starts. The cap ramps up by one after three consecutive clean items (never while a rate-limit warning is latched) and drops by one on any dirty item; enforcement is the loop body's own arithmetic. | | `work_loop_item_cap_ceiling` | number
*min 1* | `3` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_CEILING` | Upper bound the work-loop lane's adaptive item cap can ramp to for non-frontier-tier items. Frontier-tier items are bounded separately by work_loop_frontier_item_cap_ceiling. | | `work_loop_item_cap_floor` | number
*min 1* | `1` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_FLOOR` | Lower bound the work-loop lane's adaptive item cap can drop to on dirty items. | -| `work_loop_frontier_item_cap_ceiling` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_FRONTIER_ITEM_CAP_CEILING` | 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. | +| `work_loop_frontier_item_cap_ceiling` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_FRONTIER_ITEM_CAP_CEILING` | Quota guard for frontier-capability-tier items in the work-loop lane: items carrying capability-tier: frontier 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 provider-permissioned label only; absent label = general tier (fail-closed). | | `work_loop_no_progress_threshold` | number
*min 1* | `3` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_NO_PROGRESS_THRESHOLD` | Consecutive no-progress cycles (actionable work in view, no item advanced and no PR opened) before the work-loop lane raises its stall escalation. The lane escalates and keeps looping; it never stops on a stall. Idle cycles with nothing actionable neither count nor reset. | ### How to set these diff --git a/plugins/work-items/reference/capability-tier-labels.md b/plugins/work-items/reference/capability-tier-labels.md new file mode 100644 index 0000000000..88e91370c4 --- /dev/null +++ b/plugins/work-items/reference/capability-tier-labels.md @@ -0,0 +1,77 @@ +# Capability-tier labels + +Canonical `capability-tier:` axis members for the work-loop frontier-tier quota guard and +triage stamping. Dispatch model selection (frontier / strong / fast) is owned by the loop-lane +convention and agent frontmatter bindings; this leaf owns the **label strings** triage stamps +when an item needs the frontier tier's throughput bound. + +## Canonical members + +| Label | Tier | Description | Color (GitHub) | +|-------|------|-------------|----------------| +| `capability-tier: frontier` | frontier | Work-loop frontier quota guard (concurrency 1, separate adaptive cap ceiling) | `5319E7` | + +Discover live members through the bound adapter's label listing (GitHub: +`gh label list --search 'capability-tier:'`). An item carries **at most one** +`capability-tier:` label. The work-loop adaptive cap reads the label only — never body prose +claiming a tier. + +**Absent label = general tier (fail-closed).** Items with no `capability-tier:` label run under +the general adaptive cap ceiling. A body or brief that mentions frontier tier is context for the +operator; it does not select the quota guard. + +**Security-surface dispatch is separate.** Security-surface work still routes to the frontier +capability tier for model selection via work-class rules in the admission gate — that path does +not substitute for the label when the quota guard needs to bind. + +## Escalation + +When an item genuinely needs the frontier quota guard and lacks the label: + +1. **Interactive triage** — apply `capability-tier: frontier` in the outcome edit when the + label exists in the repo. +2. **Autonomous lane** — note the need in the triage comment and leave the label unstamped when + the repo cannot provision it; the work-loop lane treats the item as general tier until triage + (or an operator) applies the label. +3. **Label-as-code owner** — route provisioning to that system (`melodic-software/github-iac` for + org repos). This plugin never creates the label ad hoc when label-as-code owns writes. + +## Migration + +Repos adopting the frontier-tier quota guard need `capability-tier: frontier` present **before** +triage stamps it. + +1. **Label-as-code owner declared** — route provisioning to that system; `/work-items:setup` + validates presence only and never writes labels ad hoc. +2. **No label-as-code owner** — `/work-items:setup apply` may create the label with an + interactive user present, using the same mechanics as the work-class axis migration. An + unattended `apply` stops with explicit remediation instead of guessing. + +Triage preflights the label before stamping; when absent, it reports remediation and omits the +label rather than inventing one. + +### Legacy body stamps (pre-#1716 backfill) + +Before the label reader flip, `work-loop` read frontier tier from triage-briefing body prose. +Triage refuses to re-triage already-triaged output, so `/work-items:setup apply` runs a one-shot +backfill after the label axis is provisioned. A body matches the legacy signal when it carries +any of these **stamp** patterns (generic security-surface dispatch prose does not match): + +- `Capability tier: frontier` or `capability-tier: frontier` in the briefing body +- `stamped for the frontier capability tier` +- `frontier-tier quota guard` as an item-level stamp (not dispatch-policy prose) +- `**Capability tier:** frontier` in an agent brief + +Detection and apply mechanics live in +[`${CLAUDE_PLUGIN_ROOT}/scripts/backfill-capability-tier-labels.sh`](${CLAUDE_PLUGIN_ROOT}/scripts/backfill-capability-tier-labels.sh) +(with pattern helpers in `scripts/lib/legacy-frontier-tier-signal.sh`). The backfill pass: + +1. **Skips** when the bound provider is not GitHub (no label listing / bulk listing) — report INFO. +2. **Skips** when `capability-tier: frontier` is absent from the repo — the label axis pass must + run first. +3. **Reports** candidates via `backfill-capability-tier-labels.sh check` (read-only). +4. **Applies** with an interactive user present: offer to run `backfill-capability-tier-labels.sh apply` + (RECOMMENDED: apply all candidates). Unattended `apply` runs `check` only and names the command + to run with a user present — never mutates items without confirmation. +5. **Label-as-code owner** — when declared, setup validates and reports candidates only; the owner + applies labels (or the operator runs backfill after IaC lands the label). diff --git a/plugins/work-items/reference/item-content-trust.md b/plugins/work-items/reference/item-content-trust.md index 71751a55fb..8d45400fac 100644 --- a/plugins/work-items/reference/item-content-trust.md +++ b/plugins/work-items/reference/item-content-trust.md @@ -38,21 +38,10 @@ admission" ([`admission-policy.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/autonomy/reference/guardrails/admission-policy.md)). A body-recorded claim that can only ever **tighten** — one that routes an item to a slower tier, a -smaller cap, a stricter gate, or a human — is not an authority input and stays usable as a signal: -believing it costs conservatism, not safety. Widening is the direction that needs an authenticated -surface. The instance shipped here is `work-loop`'s frontier-tier quota guard ("Adaptive item cap"): -its tier signal comes from the triage briefing in the item body, and what the guard does with that -signal is restrictive at the shipped defaults — concurrency 1, and a frontier cap ceiling below the -general one. It qualifies only while that ordering holds: an operator who configures the frontier -ceiling *above* the general one has made the ceiling half of the guard widen throughput, and that -half stops being an instance of this carve-out — the consuming site drops the separate ceiling and -bounds the item by the general one, keeping the concurrency-1 half, which can only tighten -([`work-loop`](../skills/work-loop/SKILL.md), "Adaptive item cap"). No other surface in this plugin -reads that signal, so a body claiming the frontier tier buys its item a throughput bound and nothing -else — no admission, no dispatch, no merge eligibility, no gate waiver. - -Reading such a claim is still worth doing where it saves an operator a re-diagnosis: relay it as -context, attributed to the body, and let the authenticated surface decide. +smaller cap, a stricter gate, or a human — is not an authority input and stays usable as relayed +context: believing it costs conservatism, not safety. Widening is the direction that needs an +authenticated surface. Reading such a claim is still worth doing where it saves an operator a +re-diagnosis: attribute it to the body and let the authenticated surface decide. ## Handing item text to a subagent @@ -86,6 +75,10 @@ These are instances of the rule above, not separate rules: - **The merge partition's work class** — read from the provider-permissioned `work-class:` label, never from a `Work-class: C` body trailer, which any item author can write about their own item (`source-control`'s `babysit-loop`, "Rung partition"). +- **Capability-tier label** — `work-loop`'s frontier-tier quota guard reads + `capability-tier: frontier` from the provider-permissioned label projection, never from triage + briefing prose in the item body ([`work-loop`](../skills/work-loop/SKILL.md), "Adaptive item + cap"; [`capability-tier-labels.md`](capability-tier-labels.md)). - **Role labels** — not ratification evidence either: unattended triage applies the autonomous-eligible label to every briefed delegable item, so carrying it proves no operator reviewed anything ([`work-loop`](../skills/work-loop/SKILL.md), "Admission gate"). diff --git a/plugins/work-items/reference/label-taxonomy.md b/plugins/work-items/reference/label-taxonomy.md index 9060ad4794..4b3d841c10 100644 --- a/plugins/work-items/reference/label-taxonomy.md +++ b/plugins/work-items/reference/label-taxonomy.md @@ -21,6 +21,7 @@ These axes work in any repo and don't change per team. Do not snapshot their mem | Meta | (none) | Tool-owned flat markers the automation sets: `automated`, `good-first-issue`, `migrated`, `stale`, plus the three canonical-role labels (defaults `agent-ready`, `needs-human`, `recurring` — see "Canonical roles" below). | | Cadence | `cadence:` | Recurrence period for maintenance items. Members from the live set. | | Work class | `work-class:` | Semantic risk class (C1–C5) stamped at triage for the fail-closed admission gate and merge partition. Canonical members and migration: [`work-class-labels.md`](work-class-labels.md). Classification criteria: the `autonomy` plugin's [`work-classes.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/autonomy/reference/guardrails/work-classes.md). | +| Capability tier | `capability-tier:` | Execution capability tier for the work-loop frontier quota guard. Canonical members and migration: [`capability-tier-labels.md`](capability-tier-labels.md). Absent label = general tier (fail-closed); body prose is never authority. | ## Canonical roles diff --git a/plugins/work-items/reference/tracker-seam.md b/plugins/work-items/reference/tracker-seam.md index 0789e5e6a6..e31ecc11ca 100644 --- a/plugins/work-items/reference/tracker-seam.md +++ b/plugins/work-items/reference/tracker-seam.md @@ -124,6 +124,7 @@ from the tracker binding's `config.role_labels` (defaults `agent-ready` / `needs | Ecosystem | `ecosystem:` | repo-specific | the consuming repo's language/toolchain mix — see [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md) | | Cadence | `cadence:` | repo-specific | e.g. `cadence:weekly`, `cadence:monthly` — members from the live set | | Work class | `work-class:` | universal | C1–C5 semantic risk class — canonical members and migration in [`${CLAUDE_PLUGIN_ROOT}/reference/work-class-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/work-class-labels.md) | +| Capability tier | `capability-tier:` | universal | frontier quota-guard tier — canonical members and migration in [`${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md); absent = general tier | ## Role-label resolution is an action-entry invariant diff --git a/plugins/work-items/scripts/backfill-capability-tier-labels.sh b/plugins/work-items/scripts/backfill-capability-tier-labels.sh new file mode 100755 index 0000000000..711171329c --- /dev/null +++ b/plugins/work-items/scripts/backfill-capability-tier-labels.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# backfill-capability-tier-labels.sh — one-shot migration for #1716 body stamps. +# +# Finds open items whose body carries a legacy frontier-tier triage-briefing signal +# but lack the provider-permissioned capability-tier: frontier label, then optionally +# applies the label. Triage refuses to re-triage already-triaged output, so setup +# apply runs this pass after the label axis is provisioned. +# +# Usage: +# backfill-capability-tier-labels.sh check [--repo /] +# backfill-capability-tier-labels.sh apply [--repo /] [--dry-run] [--yes] +# +# check — prints one candidate issue/PR number per line (stdout); exit 0 always. +# apply — adds capability-tier: frontier to each candidate; exit 1 when label missing +# from repo or gh unavailable; exit 2 when interactive confirmation declined. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/lib" +# shellcheck source=lib/legacy-frontier-tier-signal.sh +source "$LIB_DIR/legacy-frontier-tier-signal.sh" + +CAPABILITY_TIER_LABEL='capability-tier: frontier' +MODE="${1:-}" +shift || true + +REPO="" +DRY_RUN=0 +ASSUME_YES=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) REPO="${2:-}"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + --yes) ASSUME_YES=1; shift ;; + *) + echo "ERROR: unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +repo_args=() +if [[ -n "$REPO" ]]; then + repo_args=(-R "$REPO") +fi + +require_gh() { + command -v gh >/dev/null 2>&1 || { + echo "ERROR: gh required for backfill" >&2 + exit 1 + } + command -v jq >/dev/null 2>&1 || { + echo "ERROR: jq required for backfill" >&2 + exit 1 + } +} + +label_exists_in_repo() { + require_gh + gh label list "${repo_args[@]}" --limit 200 --json name \ + | jq -e --arg want "$CAPABILITY_TIER_LABEL" '[.[] | .name] | index($want) != null' >/dev/null +} + +item_has_capability_tier_label() { + local labels_json="$1" + jq -e --arg want "$CAPABILITY_TIER_LABEL" \ + '[.[] | .name] | index($want) != null' <<<"$labels_json" >/dev/null 2>&1 +} + +list_open_items_without_label_json() { + require_gh + local items + items="$(gh issue list "${repo_args[@]}" --state open \ + --json number,title,body,labels \ + --limit 1000 | tr -d '\r')" + + jq -c --arg label "$CAPABILITY_TIER_LABEL" ' + [.[] | + select( + ([.labels[]?.name] | index($label) == null) + and (.body != null) + ) + ]' <<<"$items" +} + +filter_legacy_candidates() { + local items_json="$1" + jq -c '.[]' <<<"$items_json" | while IFS= read -r item; do + local body labels_json number + body="$(jq -r '.body // ""' <<<"$item")" + labels_json="$(jq -c '.labels // []' <<<"$item")" + number="$(jq -r '.number' <<<"$item")" + # shellcheck disable=SC2310 # jq probe; false means "no label", not a fault + if item_has_capability_tier_label "$labels_json"; then + continue + fi + # shellcheck disable=SC2310 # regex probe; false means "no legacy stamp", not a fault + if wit_body_has_legacy_frontier_tier_signal "$body"; then + printf '%s\n' "$number" + fi + done +} + +usage() { + cat </] + backfill-capability-tier-labels.sh apply [--repo /] [--dry-run] [--yes] +EOF +} + +case "$MODE" in + check) + items="$(list_open_items_without_label_json)" + filter_legacy_candidates "$items" + ;; + apply) + # shellcheck disable=SC2310 # gh probe; false means "label missing", handled below + if ! label_exists_in_repo; then + echo "ERROR: $CAPABILITY_TIER_LABEL is not provisioned in the repository label set" >&2 + echo "Run /work-items:setup apply to provision the label axis first, or route to the label-as-code owner." >&2 + exit 1 + fi + mapfile -t candidates < <(items="$(list_open_items_without_label_json)"; filter_legacy_candidates "$items") + if [[ "${#candidates[@]}" -eq 0 ]]; then + echo "No legacy frontier-tier body stamps need backfill." + exit 0 + fi + if [[ "$DRY_RUN" -eq 1 ]]; then + printf 'Would apply %s to issue(s): %s\n' "$CAPABILITY_TIER_LABEL" "${candidates[*]}" + exit 0 + fi + if [[ "$ASSUME_YES" -ne 1 ]]; then + echo "Apply $CAPABILITY_TIER_LABEL to ${#candidates[@]} item(s): ${candidates[*]}?" + read -r -p "Proceed? [y/N] " reply + case "$reply" in + [yY]|[yY][eE][sS]) ;; + *) echo "Aborted." >&2; exit 2 ;; + esac + fi + for number in "${candidates[@]}"; do + gh issue edit "$number" "${repo_args[@]}" --add-label "$CAPABILITY_TIER_LABEL" + echo "Applied $CAPABILITY_TIER_LABEL to #$number" + done + ;; + *) + usage >&2 + exit 1 + ;; +esac diff --git a/plugins/work-items/scripts/backfill-capability-tier-labels.test.sh b/plugins/work-items/scripts/backfill-capability-tier-labels.test.sh new file mode 100755 index 0000000000..f2ee374ee9 --- /dev/null +++ b/plugins/work-items/scripts/backfill-capability-tier-labels.test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKFILL="$SCRIPT_DIR/backfill-capability-tier-labels.sh" +LIB_TEST="$SCRIPT_DIR/lib/legacy-frontier-tier-signal.test.sh" + +FAILED=0 +pass() { printf 'PASS: %s\n' "$1"; } +fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; } + +chmod +x "$BACKFILL" "$LIB_TEST" + +if bash "$LIB_TEST"; then + pass "legacy signal unit tests" +else + fail "legacy signal unit tests" +fi + +# Offline: apply without gh should fail clearly. +if "$BACKFILL" apply --dry-run 2>/dev/null; then + fail "apply without gh should not succeed" +else + pass "apply without gh exits non-zero" +fi + +if [[ "$FAILED" -eq 0 ]]; then + exit 0 +fi +exit 1 diff --git a/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.sh b/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.sh new file mode 100755 index 0000000000..519e5630b4 --- /dev/null +++ b/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# legacy-frontier-tier-signal.sh — detect pre-#1716 frontier-tier body stamps. +# +# Before the label reader flip, work-loop read frontier tier from triage-briefing +# body prose. Triage refuses to re-triage already-triaged items, so setup's +# backfill pass uses these patterns to find items that still carry only the legacy +# signal. Patterns are conservative: generic "frontier tier" mentions (e.g. +# security-surface dispatch prose) do not match. + +wit_body_has_legacy_frontier_tier_signal() { + local body="${1:-}" + [[ -n "$body" ]] || return 1 + + local -a patterns=( + '[Cc]apability[- ]tier:[[:space:]]*[Ff]rontier' + '[Ss]tamped for the frontier capability tier' + '[Ff]rontier[- ]tier quota guard' + '\*\*[Cc]apability tier:\*\*[[:space:]]*[Ff]rontier' + '[Cc]apability tier:[[:space:]]*[Ff]rontier[[:space:]]*\(quota guard\)' + ) + + local pattern + for pattern in "${patterns[@]}"; do + if printf '%s' "$body" | grep -Eq "$pattern"; then + return 0 + fi + done + return 1 +} diff --git a/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.test.sh b/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.test.sh new file mode 100755 index 0000000000..c110a642f3 --- /dev/null +++ b/plugins/work-items/scripts/lib/legacy-frontier-tier-signal.test.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=legacy-frontier-tier-signal.sh +source "$SCRIPT_DIR/legacy-frontier-tier-signal.sh" + +FAILED=0 +pass() { printf 'PASS: %s\n' "$1"; } +fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; } + +assert_match() { + local name="$1" body="$2" + if wit_body_has_legacy_frontier_tier_signal "$body"; then + pass "$name" + else + fail "$name" + fi +} + +assert_no_match() { + local name="$1" body="$2" + if wit_body_has_legacy_frontier_tier_signal "$body"; then + fail "$name" + else + pass "$name" + fi +} + +assert_match "capability tier colon frontier" $'## Agent Brief\nCapability tier: frontier\n' +assert_match "capability-tier label form in body" $'capability-tier: frontier\n' +assert_match "stamped for the frontier capability tier" \ + 'Item stamped for the frontier capability tier during triage.' +assert_match "frontier-tier quota guard phrase" \ + 'This item runs under the frontier-tier quota guard.' +assert_match "bold capability tier in brief" $'**Capability tier:** frontier\n' + +assert_no_match "empty body" '' +assert_no_match "generic security-surface dispatch mention" \ + 'Security-surface work routes to the frontier capability tier for model selection.' +assert_no_match "already has label only in labels not body" \ + 'Fix the README link.' + +if [[ "$FAILED" -eq 0 ]]; then + exit 0 +fi +exit 1 diff --git a/plugins/work-items/skills/setup/SKILL.md b/plugins/work-items/skills/setup/SKILL.md index ff9f297ef2..4bb6ba784c 100644 --- a/plugins/work-items/skills/setup/SKILL.md +++ b/plugins/work-items/skills/setup/SKILL.md @@ -1,5 +1,5 @@ --- -description: "Verify and configure the work-items plugin for this repository. check inspects read-only the tracker provider binding (.work-item-tracker.json), the tracked .github/recurring-schedule.json (presence, JSON validity, unique reconciliation keys), the jq and tracker-seam entry gates, the recurring-maintenance role label, and the work-class label axis; apply binds the tracker provider (seeds .work-item-tracker.json with the provider + non-secret config), writes the schedule, migrates missing work-class labels when authorized, and optionally remaps the canonical role labels in the tracker binding. On a first-time bind apply writes that minimum viable config only — binding, role-label pass, work-class migration pass, empty schedule skeleton — and the pass that infers candidates from the repo and interviews the consumer for their recurring work items is opt-in, via the --seed-schedule argument or a single offer whose recommended default is skip (applied silently with no interactive user); a schedule that already carries items is summarized and offered updates exactly as before. Use when: 'set up work-items', 'bind the tracker provider', 'is work-items configured', 'configure the recurring schedule', 'work-items setup', 'seed recurring items', 'bulk-seed the recurring schedule', 'remap the work-item role labels', or the due/recheck/work actions report no recurring schedule configured, or the seam reports no binding. Re-runnable — safe to invoke again to reconfigure or to seed the schedule later." +description: "Verify and configure the work-items plugin for this repo. check read-only inspects the tracker binding (.work-item-tracker.json), tracked .github/recurring-schedule.json (presence, JSON validity, unique reconciliation keys), jq and tracker-seam entry gates, recurring-maintenance role label, work-class axis, and capability-tier axis; apply binds the provider, writes the schedule, migrates work-class and capability-tier labels when authorized, backfills legacy frontier stamps to the label, and optionally remaps canonical role labels. First-time bind writes minimum viable config only — binding, role labels, both label axes, legacy backfill, empty skeleton — and candidate inference plus per-item interview is opt-in via --seed-schedule or a skip-RECOMMENDED offer (silent when unattended); a schedule with items is summarized and offered updates as before. Use when: 'set up work-items', 'bind the tracker provider', 'is work-items configured', 'configure the recurring schedule', 'work-items setup', 'seed recurring items', 'bulk-seed the recurring schedule', 'remap the work-item role labels', or the due/recheck/work actions report no recurring schedule configured, or the seam reports no binding. Re-runnable — safe to invoke again to reconfigure or to seed the schedule later." argument-hint: "check | apply [--seed-schedule] [--accept-recommended]" user-invocable: true disable-model-invocation: true @@ -188,11 +188,11 @@ check. `config.role_labels["recurring-maintenance"]` (default `recurring` when the entry is absent; a malformed, empty, or non-string configured value is FAIL); missing `cadence:{cadence}` labels are taxonomy niceties — INFO. **That resolution FAIL settles probe 6 outright** — it is a binding - error, independent of any schedule, and `apply` step 7 calls the same value "an error, not a + error, independent of any schedule, and `apply` step 9 calls the same value "an error, not a fallback" at any row count. The branches below decide only whether a *resolved* label's absence is a gate, so reach them only once the role resolves; the probe emits one verdict, and letting a row count that is zero, absent, or unreadable pick INFO would drop the binding error from the table - entirely. With the role resolved, branch on the schedule's **row count**, exactly as `apply` step 7 + entirely. With the role resolved, branch on the schedule's **row count**, exactly as `apply` step 9 does — never on whether the schedule file exists. The skipped first-time bind leaves a present-but-empty `{"items": []}` on disk, so a file-presence gate hard-FAILs the expected post-bind steady state over an item that can never be created: @@ -218,6 +218,16 @@ check. Providers without a label listing (`local-markdown`, read-only `jira`) → INFO: verify at triage time via the item store. When probe 2 is INFO (no binding) or FAIL (malformed binding), skip this probe — there is no addressable provider yet. +8. **Capability-tier label axis** — when probe 2 found a present, shape-valid binding whose provider + exposes label listing (the `github` adapter: `gh label list`), verify the canonical + `capability-tier: frontier` member from + [`${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md) + exists. Absent is FAIL — triage cannot stamp frontier-tier quota guard and the work-loop reader + fails closed to general tier until the label exists; remediation is `/work-items:setup apply` on + repos without label-as-code, or the repo's declared label-as-code owner when one exists (never + `gh label create` ad hoc there). Providers without a label listing (`local-markdown`, read-only + `jira`) → INFO: verify at triage time via the item store. When probe 2 is INFO (no binding) or + FAIL (malformed binding), skip this probe — there is no addressable provider yet. ## `apply` (idempotent) @@ -227,7 +237,7 @@ skipped first-time `apply` leaves `{"items": []}` on disk, so a file-absence gat seeding path unreachable by re-running: - **Schedule carries ≥1 item** — unchanged from before: summarize it, infer candidates, and interview - against that baseline (steps 5–7), offering updates. `--seed-schedule` is a no-op here; this branch + against that baseline (steps 7–9), offering updates. `--seed-schedule` is a no-op here; this branch already interviews. - **Schedule absent, or present with an empty `items` array** — write only the minimum viable config: the provider binding, the role-label pass, and the empty `{"items": []}` skeleton so `due` / @@ -241,10 +251,10 @@ seeding path unreachable by re-running: Seeding rows on the empty/absent branch is **opt-in**, satisfied by any one of: the explicit `--seed-schedule` argument; an accepted yes/no offer; or an invocation that in its own words asks for the schedule to be seeded (e.g. "seed a sensible recurring schedule for this repo") — an explicit -request IS the opt-in, so honor it without re-asking. Otherwise offer exactly once, before step 5, as a +request IS the opt-in, so honor it without re-asking. Otherwise offer exactly once, before step 7, as a single yes/no with **skip marked RECOMMENDED**: name that seeding walks them through one interview per candidate item, that the skeleton alone already stops the degradation, and that re-running `apply` (or -`apply --seed-schedule`) bulk-seeds later at any time. On skip, say so plainly and go to step 8. +`apply --seed-schedule`) bulk-seeds later at any time. On skip, say so plainly and go to step 10. ### Autonomous invocation (no interactive user) @@ -266,14 +276,16 @@ Applied to the three passes: | Provider binding (`apply` step 1, which runs the "Provider binding" procedure) | **Binding already present and valid — keep it, and re-bind nothing.** That is the procedure's own read-first RECOMMENDED answer, so this rule resolves to it silently: a repo bound to `local-markdown`, `jira`, or a consumer-local provider stays on it, and a working `gh` never switches it to `github`. Re-binding is a switch-providers decision, which no default can stand in for. (A present binding the probe already FAILs never reaches here — `apply` runs `check` first, and that probe FAILs a malformed shape, a provider resolving to no adapter, a missing required config key, and a `github` binding this checkout cannot derive a repo for.) **Binding absent** — bind `github` with `config.lease_ttl_hours: 24`, both RECOMMENDED, **only when `gh` is installed AND `gh repo view --json owner,name` resolves in this checkout**. The old test was `gh auth status`, which proves only that an account is authenticated somewhere — never that this repository is hosted on GitHub, so a local-only or non-GitHub checkout was bound to a provider whose every repo-scoped verb then fails. `gh repo view` is the adapter's own derivation and the operative test: it subsumes authentication for the host this checkout uses, and it is not the machine-wide check `gh auth status` is (that one tests every account on every known host and exits 1 if any has an issue, per `gh auth status --help`, so an unrelated stale credential would refuse a good bind). Report the resolved `owner/repo` in the summary alongside the other defaults taken. Otherwise stop: `local-markdown` and `jira` need `storage_dir` / `config.jira` values that have no defaults and cannot be inferred, so there is no provider left to choose safely. Report "tracker binding needs a provider decision; run `/work-items:setup apply` with a user present". | | Role labels (step 2) | Keep the defaults — the RECOMMENDED answer, and the one that writes nothing. The pass runs and completes as a no-op: `config.role_labels` is left absent, so every role resolves to its documented fallback. A remap is a repo-vocabulary decision no default can stand in for. | | Work-class labels (step 3) | When any canonical member is missing: if the repo declares a label-as-code owner, stop — name the missing labels and point remediation at that owner. Otherwise stop: "work-class axis needs provisioning; run `/work-items:setup apply` with a user present". Never create labels ad hoc unattended. | -| Schedule seeding (before step 5) | Skip — the RECOMMENDED answer. Write the empty `{"items": []}` skeleton and go to step 8. **Exception:** when the invocation carries both `--seed-schedule` and `--accept-recommended`, run steps 5–6 using each inferred candidate's recommended values without per-item interviews (unattended bulk seed). | +| Capability-tier labels (step 4) | When `capability-tier: frontier` is missing: if the repo declares a label-as-code owner, stop — name the missing label and point remediation at that owner. Otherwise stop: "capability-tier axis needs provisioning; run `/work-items:setup apply` with a user present". Never create labels ad hoc unattended. | +| Legacy capability-tier backfill (step 5) | Unattended: run `backfill-capability-tier-labels.sh check` only and report candidates with the apply command for a user-present run. Never mutate item labels without confirmation. | +| Schedule seeding (before step 7) | Skip — the RECOMMENDED answer. Write the empty `{"items": []}` skeleton and go to step 10. **Exception:** when the invocation carries both `--seed-schedule` and `--accept-recommended`, run steps 7–8 using each inferred candidate's recommended values without per-item interviews (unattended bulk seed). | So an autonomous first-time bind on a `gh`-ready repo produces the binding, the role-label pass, and the empty skeleton, and nothing else; an autonomous re-run against a repo that is already bound leaves that binding exactly as it found it. Absent an opt-in, never infer and never interview. `--seed-schedule` carries the opt-in decision without the offer prompt, but the pass it selects is -step 5's per-item interview — so it is not a non-interactive seeding path unless `--accept-recommended` -is also present. Pairing both flags tells step 6 to accept every inferred candidate with its +step 7's per-item interview — so it is not a non-interactive seeding path unless `--accept-recommended` +is also present. Pairing both flags tells step 8 to accept every inferred candidate with its recommended cadence/title fields and write the schedule without blocking on questions (#1302). The row shape, the root `{"items": []}` structure, and the cadence-duration table are defined once in @@ -292,14 +304,22 @@ unambiguous; ask only where an item genuinely needs the user. 3. **Migrate the work-class label axis.** Run the "Work-class label axis (migration)" procedure below. It discovers missing canonical members and provisions them when authorized. When any member is still missing after this pass, stop — triage and the work-loop admission gate cannot operate correctly. -4. **Read the current schedule file first.** If `.github/recurring-schedule.json` exists, load it and +4. **Migrate the capability-tier label axis.** Run the procedure in + [reference/capability-tier-axis-migration.md](reference/capability-tier-axis-migration.md). When the + canonical member is still missing after this pass, stop — triage cannot stamp frontier-tier quota + guard and the work-loop reader fails closed to general tier. +5. **Backfill legacy frontier-tier body stamps.** Run the procedure in + [reference/capability-tier-backfill.md](reference/capability-tier-backfill.md). This pass is + load-bearing on upgrade (#1716): items already triaged with only a body prose frontier-tier stamp + will not be re-triaged, so setup applies the label here once the axis exists. +6. **Read the current schedule file first.** If `.github/recurring-schedule.json` exists, load it and present a short summary (item count, each item's `id` / `cadence` / `next_due`, and which are already overdue against today). The interview proposes changes against that baseline; nothing is dropped without the user confirming. If the file is absent or carries an empty `items` array, say so and - settle the opt-in decision above before steps 5–6. -5. **Infer candidate items before asking — steps 5 and 6 run on the seeding path only** (the schedule + settle the opt-in decision above before steps 7–8. +7. **Infer candidate items before asking — steps 7 and 8 run on the seeding path only** (the schedule already carries ≥1 item, or seeding was opted into). On the default skipped path, run neither and go - straight to step 7. Recurring items can't be fully derived, but don't skip the rung — propose + straight to step 9. Recurring items can't be fully derived, but don't skip the rung — propose candidates from what the repo actually contains, each with a recommended cadence: - Dependency manifests (`package.json`, `*.csproj` / `Directory.Packages.props`, `pyproject.toml`, `Cargo.toml`, `go.mod`) → a "Review dependency manifest / check for updates" item (recommend @@ -312,9 +332,9 @@ unambiguous; ask only where an item genuinely needs the user. (recommend `semi-annual` or `quarterly`). Present these as a starting menu; the user keeps, edits, or drops each. Do not invent items the repo gives no signal for. -6. **Interview, one decision at a time, recommendation first.** When `--accept-recommended` is set - alongside `--seed-schedule`, skip the interview: accept every inferred candidate from step 5 with - its recommended field values and proceed to step 7. Otherwise, for each candidate (and any custom item +8. **Interview, one decision at a time, recommendation first.** When `--accept-recommended` is set + alongside `--seed-schedule`, skip the interview: accept every inferred candidate from step 7 with + its recommended field values and proceed to step 9. Otherwise, for each candidate (and any custom item the user names last), settle its fields against the shape in [`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/add.md): `id` (kebab-case), `title`, `cadence` (one of the cadence table's values), `area[]`, `category`, `triggers[]` (external @@ -330,7 +350,7 @@ unambiguous; ask only where an item genuinely needs the user. `last_checked` to today (setup did no maintenance). Blindly resetting the dates would drop an already-overdue item out of the `due` / `work` recurring tiers, which both select on `next_due <= today`. -7. **Confirm the recurring-maintenance role label is present in the provider — load-bearing whenever +9. **Confirm the recurring-maintenance role label is present in the provider — load-bearing whenever the schedule will carry rows.** (Step 2 settled which label string each role resolves to; this step verifies that string actually exists.) Key this on the schedule's **final row count**, not on what this run wrote: with ≥1 row (written now or already on disk) a missing label is reported as a hard @@ -354,11 +374,11 @@ unambiguous; ask only where an item genuinely needs the user. recurring automation or the `work` due-recurring tier when `next_due` arrives. Do **not** point users at `add --recurring` to create it: that per-item path appends another schedule row, duplicating an already-seeded item. -8. **Write the schedule.** On the skipped path there is nothing to merge: write the `{"items": []}` - skeleton when the file is absent, leave an already-empty file untouched, and go to step 10 — step 9 +10. **Write the schedule.** On the skipped path there is nothing to merge: write the `{"items": []}` + skeleton when the file is absent, leave an already-empty file untouched, and go to step 12 — step 11 has no renamed or dropped row to reconcile. Otherwise read the current file (if any) and merge the accepted items into the `items` - array, keying each edited item on the **original `id` it had when read in step 4**, not its final + array, keying each edited item on the **original `id` it had when read in step 6**, not its final `id` — so an id rename replaces the original row instead of leaving it behind. Concretely: replace the row whose id matches the item's origin id; append only genuinely new items (no origin row); and when the user renamed an id, drop the old-id row so `due` / `work` never see two rows for the same @@ -368,7 +388,7 @@ unambiguous; ask only where an item genuinely needs the user. two rows, replace one, or pick a unique value; never write a schedule with a duplicate `id` or `title`. Then write it back with the `{"items": [ ... ]}` root. Confirm the file is tracked, not ignored. -9. **Reconcile an existing row's open item when it is renamed OR dropped.** Both operations strand the +11. **Reconcile an existing row's open item when it is renamed OR dropped.** Both operations strand the row's live `[Maintenance] {old title}` recurring item (if still open): after write the schedule no longer carries that title, so `due` / `work` — which derive recurring candidates only from the schedule, and whose frontier tiers exclude items carrying the resolved recurring-maintenance label — will never surface it again, @@ -379,19 +399,18 @@ unambiguous; ask only where an item genuinely needs the user. so it can return a longer item (`[Maintenance] Review CI workflow pins`) when the old title was `Review CI` — **filter the results to the one whose title equals `[Maintenance] {old title}` exactly** before acting, and never reconcile against a mere prefix/substring match. When exactly one - exact match exists: - - **Renamed row:** rename that item to `[Maintenance] {new title}` (a provider title-edit op — - GitHub adapter: `gh issue edit --title ...`) to keep the reconciliation key consistent, or - close it (adapter: "Close item") if the user is instead retiring the item. - - **Dropped row:** close that item (adapter: "Close item") with a comment noting the recurring item - was retired from the schedule — otherwise the `recurring`-labeled issue lingers unreachable. + exact match exists — **Renamed row:** rename that item to `[Maintenance] {new title}` (a provider title-edit op — + GitHub adapter: `gh issue edit --title ...`) to keep the reconciliation key consistent, or + close it (adapter: "Close item") if the user is instead retiring the item; **Dropped row:** close that item (adapter: "Close item") with a comment noting the recurring item + was retired from the schedule — otherwise the `recurring`-labeled issue lingers unreachable. A rename or drop with no exact-match open item needs no reconciliation. -10. **Verify after remediation.** Re-run the `check` probes on the written binding and schedule — binding - validity, including that any `config.role_labels` step 2 wrote survived the step-8 write intact and + +12. **Verify after remediation.** Re-run the `check` probes on the written binding and schedule — binding + validity, including that any `config.role_labels` step 2 wrote survived the step-10 write intact and is well-formed; JSON validity; unique `id`/`title`; tracked-not-ignored — and report the actual - results, never success on the write alone. This re-run is scoped to those probes: step 7 already + results, never success on the write alone. This re-run is scoped to those probes: step 9 already owns whether the resolved recurring-maintenance label exists in the provider, so do not repeat that - lookup here; step 3 already owns work-class axis provisioning. + lookup here; steps 3–4 already own work-class and capability-tier axis provisioning. ## Work-class label axis (migration) diff --git a/plugins/work-items/skills/setup/evals/evals.json b/plugins/work-items/skills/setup/evals/evals.json index 66850436f7..4386caf876 100644 --- a/plugins/work-items/skills/setup/evals/evals.json +++ b/plugins/work-items/skills/setup/evals/evals.json @@ -173,6 +173,32 @@ "Does not create labels ad hoc", "Writes no file" ] + }, + { + "id": 14, + "name": "check-missing-capability-tier-label-fails", + "prompt": "/work-items:setup check\n\n(.work-item-tracker.json is bound to github, gh label list returns no capability-tier: labels, and the schedule is present with zero rows.)", + "expected_output": "Probe 8 reports FAIL for the missing capability-tier: frontier label — triage cannot stamp frontier-tier quota guard and the work-loop reader fails closed to general tier until the label exists. Remediation is /work-items:setup apply on repos without label-as-code, or the repo's declared label-as-code owner. Never gh label create ad hoc when label-as-code owns writes. Nothing is written.", + "files": [], + "expectations": [ + "Reports the capability-tier label probe as FAIL when capability-tier: frontier is absent", + "Names /work-items:setup apply or the label-as-code owner as remediation", + "Does not create labels ad hoc", + "Writes no file" + ] + }, + { + "id": 15, + "name": "apply-backfills-legacy-frontier-body-stamps", + "prompt": "/work-items:setup apply\n\n(.work-item-tracker.json is bound to github, capability-tier: frontier exists in the label set, and backfill-capability-tier-labels.sh check reports two open issues whose bodies carry legacy frontier-tier stamps but lack the label. An interactive user is present.)", + "expected_output": "After the capability-tier axis pass confirms the label exists, setup runs the legacy backfill pass: it reports the two candidates from backfill-capability-tier-labels.sh check, offers to apply capability-tier: frontier to each, and on acceptance runs backfill-capability-tier-labels.sh apply so both items receive the label. It does not rely on triage to re-stamp already-triaged items.", + "files": [], + "expectations": [ + "Runs backfill-capability-tier-labels.sh check after the capability-tier axis is provisioned", + "Offers to apply the label to each legacy body-stamped candidate", + "Uses backfill-capability-tier-labels.sh apply rather than re-triaging the items", + "Reports how many items received the label" + ] } ] } diff --git a/plugins/work-items/skills/setup/reference/capability-tier-axis-migration.md b/plugins/work-items/skills/setup/reference/capability-tier-axis-migration.md new file mode 100644 index 0000000000..f313d46441 --- /dev/null +++ b/plugins/work-items/skills/setup/reference/capability-tier-axis-migration.md @@ -0,0 +1,23 @@ +# Capability-tier label axis (migration) + +`apply` runs this pass at **step 4** of its numbered flow, after the work-class pass and before the +legacy backfill. Triage's capability-tier stamp and the work-loop frontier quota guard require +`capability-tier: frontier` from +[`${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md). + +1. **Skip when `.work-item-tracker.json` is absent** — nothing is bound yet. +2. **Skip when the bound provider has no label listing** (`local-markdown`, read-only `jira`) — report + INFO and continue; triage verifies at item-edit time. +3. **Discover** via the adapter's label listing (GitHub: `gh label list --limit 200`, filter + `capability-tier:`). Compare against the canonical member in the reference. +4. **Present** — report "capability-tier axis provisioned" and continue. +5. **Missing — label-as-code owner declared** — stop. Name the missing label and route remediation to + that owner; never `gh label create` ad hoc. +6. **Missing — no label-as-code owner, interactive user present** — offer to create the label via the + adapter's label-creation mechanics (GitHub: `gh label create "capability-tier: frontier" + --description "" --color ""` using the reference table). RECOMMENDED: create + it — this pass is the upgrade migration for repos adopting the #1716 reader flip. Re-list after + creation and confirm the member exists before continuing. +7. **Missing — no label-as-code owner, no interactive user** — stop per `apply`'s "Autonomous + invocation" rule: "capability-tier axis needs provisioning; run `/work-items:setup apply` with a + user present". diff --git a/plugins/work-items/skills/setup/reference/capability-tier-backfill.md b/plugins/work-items/skills/setup/reference/capability-tier-backfill.md new file mode 100644 index 0000000000..a023ebb242 --- /dev/null +++ b/plugins/work-items/skills/setup/reference/capability-tier-backfill.md @@ -0,0 +1,28 @@ +# Capability-tier label backfill (migration) + +`apply` runs this pass at **step 5**, immediately after the capability-tier axis pass. It is +load-bearing on upgrade: triage refuses to re-triage already-triaged output, so items stamped in-body +before #1716 need the provider-permissioned label applied here. Pattern semantics and the script path +live in the reference's "Legacy body stamps" subsection. + +Resolve the script: + +```bash +BACKFILL="${CLAUDE_PLUGIN_ROOT}/scripts/backfill-capability-tier-labels.sh" +[[ -f "$BACKFILL" ]] || BACKFILL="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}/plugins/work-items/scripts/backfill-capability-tier-labels.sh" +``` + +1. **Skip when `.work-item-tracker.json` is absent** — nothing is bound yet. +2. **Skip when the bound provider has no label listing or bulk open-item listing** (`local-markdown`, + read-only `jira`) — report INFO; backfill requires GitHub-style listing. +3. **Skip when `capability-tier: frontier` is absent from the repo** — the axis pass must provision it + first; report that backfill is blocked until the label exists. +4. **Discover** via `"$BACKFILL" check` (read-only). Report each candidate number; zero candidates → + "no legacy frontier-tier body stamps need backfill" and continue. +5. **Label-as-code owner declared** — report candidates only; route item label writes to that owner or + to an operator-run `"$BACKFILL" apply` after IaC lands the label. Do not mutate items ad hoc. +6. **Interactive user present** — offer to run `"$BACKFILL" apply` (RECOMMENDED: apply all candidates). + Confirm the count applied matches the check output. +7. **No interactive user** — report candidates and name `"$BACKFILL" apply` (or re-run + `/work-items:setup apply` with a user present) as the remediation; never mutate without + confirmation. diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index 9c02f8ebe2..858801c6ad 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -165,6 +165,8 @@ The item-side comment alone is never sufficient; stopping after step 1 leaves th **Work-class pairing (hard).** Every mutation that applies the autonomous-eligible role label (`agent-ready` by default) MUST also apply exactly one `work-class:` label in the same edit (`work-class: read-only` / `mechanical` / `scoped` / `structural` / `untrusted-provenance` — map C1–C5). Applying `agent-ready` without a work-class is a triage defect: the fail-closed admission gate then makes the item unreachable while it still looks frontier-available (medley#1677). **Classify** from the risk-property bundle — when the `autonomy` plugin is installed, read [`work-classes.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/autonomy/reference/guardrails/work-classes.md) (same reference the work-loop admission gate cites); otherwise use the label→class mapping in [`${CLAUDE_PLUGIN_ROOT}/reference/work-class-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/work-class-labels.md). **Preflight:** before any autonomous-eligible outcome, verify all five canonical labels exist per that reference's "Migration" section; if any are missing, stop without mutating and report remediation — `/work-items:setup apply` provisions them on repos without label-as-code, or route to the repo's declared label-as-code owner. +**Capability-tier stamp.** When triage assesses an item for the frontier capability tier, apply the provider-permissioned `capability-tier: frontier` label in the same mutation batch as other triage labels — never encode the tier only in briefing body prose. Body mentions of frontier tier are context for operators; `work-loop` reads the label only (#1716). Preflight per [`${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md) "Migration": if the label is missing from the repo, stop without inventing it and report provisioning (label-as-code owner or `/work-items:setup`). Security-surface work routes to the frontier dispatch tier via work-class rules without requiring this stamp. + 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 — absent entries fall back to documented defaults silently, 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. When triage spawns follow-up work — a fresh, orthogonal problem it surfaces but will not fix this pass, distinct from the item under evaluation and from work it has already scoped and routed — item creation goes through the seam `create-item` verb (`/work-items:track add` is the canonical path) and follows the shared self-observation contract ([`${CLAUDE_PLUGIN_ROOT}/reference/dogfood-filing.md`](${CLAUDE_PLUGIN_ROOT}/reference/dogfood-filing.md): dedupe → categorize → fixed shape → `needs-triage`). That new item is genuinely raw intake, so `needs-triage` is correct for it; the item triage is *evaluating* is never sent back to raw intake — its raw marker is cleared by the closing invariant below — and follow-up whose scope triage has already decided is routed through the outcome labels above, not filed as a self-observation. diff --git a/plugins/work-items/skills/work-loop/SKILL.md b/plugins/work-items/skills/work-loop/SKILL.md index d564c5c828..de023b66af 100644 --- a/plugins/work-items/skills/work-loop/SKILL.md +++ b/plugins/work-items/skills/work-loop/SKILL.md @@ -389,17 +389,16 @@ apply the manifest default: - **+1** after 3 consecutive **clean** items, up to `${user_config.work_loop_item_cap_ceiling}` (default 3). Never ramp up while `rate_limit_latch` is set. - **−1** on any **dirty** item, down to `${user_config.work_loop_item_cap_floor}` (default 1). -- **Frontier-tier quota guard:** items stamped for the frontier capability tier (tier signal from - the triage briefing — the issue body, not a label) run at **concurrency 1** with adaptive - ceiling `${user_config.work_loop_frontier_item_cap_ceiling}` (default 2); the general ceiling - applies to non-frontier tiers only. That separate ceiling rests on a body-sourced signal, so it - is a tightening-only carve-out and holds **only while the resolved frontier ceiling is ≤ the - resolved general one** — resolve both by the rule above before comparing, since an operator - inverts the ordering by raising either key or lowering the other. When frontier resolves higher, - the separate ceiling would *widen* throughput on a claim the item's own author can write: drop it - and bound the item by the general ceiling instead. Concurrency 1 still applies, because it can - only tighten ([`item-content-trust.md`](${CLAUDE_PLUGIN_ROOT}/reference/item-content-trust.md), - "Trust never widens on item text"). +- **Frontier-tier quota guard:** items wearing the provider-permissioned + `capability-tier: frontier` label (returned on `list-frontier` label projections) run at + **concurrency 1** with adaptive ceiling `${user_config.work_loop_frontier_item_cap_ceiling}` + (default 2); the general ceiling applies when that label is absent. Missing label → general + tier (fail-closed). A body/briefing claim of frontier tier is context only — relay it for the + operator; never honor it as the signal ([`item-content-trust.md`](${CLAUDE_PLUGIN_ROOT}/reference/item-content-trust.md), + [`capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md)). + Escalation: request triage apply the label ([`capability-tier-labels.md`](${CLAUDE_PLUGIN_ROOT}/reference/capability-tier-labels.md) + "Escalation"). Security-surface classes still auto-route to the frontier dispatch tier per the + convention's tier rules — independent of this label. **Clean** = the item's pipeline verdict passed and its PR opened without gate failures. **Dirty** = a failed verdict or gate, an escalation off the item mid-execution, or a seam exit 8 diff --git a/plugins/work-items/skills/work-loop/evals/evals.json b/plugins/work-items/skills/work-loop/evals/evals.json index 472c65b1fb..3047a04f90 100644 --- a/plugins/work-items/skills/work-loop/evals/evals.json +++ b/plugins/work-items/skills/work-loop/evals/evals.json @@ -23,21 +23,22 @@ { "id": 2, "name": "work-loop-adaptive-cap-guard-floor-and-572-provisioning", - "prompt": "/work-items:work-loop\n\nDurable state: item_cap=5, clean_streak=2, rate_limit_latch=false. work_loop_item_cap_ceiling is unset (manifest default 3). This cycle one item completes clean and a second returns a failed verdict after a seam exit 8 backoff. Mid-cycle the tee file at ~/.claude/rate-limit-guard/rate-limits.json shows five_hour used_percentage 93 with a fresh captured_at. One admitted item is stamped frontier-tier in its triage briefing.", - "expected_output": "The cycle clamps the persisted item_cap (5) to the resolved ceiling (3) after the durable-state re-read, reports the correction, reports the correction, then after dirty/clean adaptations persists the final post-outcome item_cap (e.g. clamped 5\u21923 then dirty \u2192 2). Execution delegates each admitted item to /work-items:work (claim/lease, dispatch, PR contract owned there), with worker-side provisioning owned by /work-items:work (landed #572): the dispatched subagent materializes the item's branch and out-of-tree worktree as its own first step and works via git -C without entering it (via /source-control:worktree's non-entering seam when installed, plain git worktree add otherwise), and the orchestrator never invokes /source-control:worktree create itself since its EnterWorktree terminal would transition the parent session; a worker that cannot provision parks the item and escalates. The frontier-tier item runs at concurrency 1 under the frontier ceiling of 2; the general ceiling applies only to non-frontier tiers. The dirty item (failed verdict; seam exit 8 counts dirty) decrements the cap toward the floor and resets the clean streak; no ramp-up occurs once the 93% trip latches rate_limit_latch. The guard floor is honored as inlined: threshold 90% of either window, pause until the tripped window's resets_at, drain-then-pause (finish in-flight, stop claiming, report), staleness rule with a mandatory session Monitor on the tee file while paused, and untrusted session fields never string-interpolated. The composed budget is stated as item cap times implement-dispatch's per-item wave cap (its internal 3\u20135 default, or the work_dispatch_concurrency_cap ceiling threaded through as --wave-cap when set, #573), with the loop body's arithmetic over the two factors bounding the fan-out. Telemetry records guard mode and the updated state block. Clamping sets the cycle starting cap only; the telemetry upsert persists the final post-outcome item_cap after dirty/clean adaptations (e.g. clamped 5\u21923 then dirty \u2192 2).", + "prompt": "/work-items:work-loop\n\nDurable state: item_cap=5, clean_streak=2, rate_limit_latch=false. work_loop_item_cap_ceiling is unset (manifest default 3). This cycle one item completes clean and a second returns a failed verdict after a seam exit 8 backoff. Mid-cycle the tee file at ~/.claude/rate-limit-guard/rate-limits.json shows five_hour used_percentage 93 with a fresh captured_at. One admitted item carries the capability-tier: frontier label.", + "expected_output": "The cycle clamps the persisted item_cap (5) to the resolved ceiling (3) after the durable-state re-read, reports the correction, then after dirty/clean adaptations persists the final post-outcome item_cap (e.g. clamped 5\u21923 then dirty \u2192 2). Execution delegates each admitted item to /work-items:work (claim/lease, dispatch, PR contract owned there), with worker-side provisioning owned by /work-items:work (landed #572): the dispatched subagent materializes the item's branch and out-of-tree worktree as its own first step and works via git -C without entering it (via /source-control:worktree's non-entering seam when installed, plain git worktree add otherwise), and the orchestrator never invokes /source-control:worktree create itself since its EnterWorktree terminal would transition the parent session; a worker that cannot provision parks the item and escalates. The item carrying capability-tier: frontier runs at concurrency 1 under the frontier ceiling of 2; the general ceiling applies only to items without that label. The dirty item (failed verdict; seam exit 8 counts dirty) decrements the cap toward the floor and resets the clean streak; no ramp-up occurs once the 93% trip latches rate_limit_latch. The guard floor is honored as inlined: threshold 90% of either window, pause until the tripped window's resets_at, drain-then-pause (finish in-flight, stop claiming, report), staleness rule with a mandatory session Monitor on the tee file while paused, and untrusted session fields never string-interpolated. The composed budget is stated as item cap times implement-dispatch's per-item wave cap (its internal 3\u20135 default, or the work_dispatch_concurrency_cap ceiling threaded through as --wave-cap when set, #573), with the loop body's arithmetic over the two factors bounding the fan-out. Telemetry records guard mode and the updated state block. Clamping sets the cycle starting cap only; the telemetry upsert persists the final post-outcome item_cap after dirty/clean adaptations (e.g. clamped 5\u21923 then dirty \u2192 2).", "files": [], "expectations": [ "Clamps persisted item_cap to the resolved [floor, ceiling] before execute (item_cap 5 with ceiling 3 \u2192 3), reports the correction, then after dirty/clean adaptations persists the final post-outcome item_cap in the telemetry state block", "Delegates per-item execution to /work-items:work without restating selection/claim/lease/dispatch machinery", "Makes worktree provisioning the dispatched worker's own first step (worker-side, owned by /work-items:work \u2014 landed #572) \u2014 the worker materializes and works via git -C without entering, and the orchestrator never invokes /source-control:worktree create itself because its EnterWorktree terminal would transition the parent session; the worker uses the worktree skill's non-entering seam when installed, plain git worktree add otherwise, and parks + escalates when it cannot provision", - "Runs the frontier-tier item at concurrency 1 with adaptive ceiling 2, keeping the general ceiling for non-frontier tiers only", + "Runs the item carrying capability-tier: frontier at concurrency 1 with adaptive ceiling 2, keeping the general ceiling for items without that label", "Counts the failed verdict / seam exit 8 as dirty: cap decrements (floor 1), clean streak resets, and backoff-and-retry applies to the exit 8", "On the 93% reading: drains in-flight work, stops claiming, pauses until the tripped window's resets_at, and arms a session Monitor on the tee file while paused", "Sets rate_limit_latch and refuses cap ramp-up while it is set", "Parses tee-file session fields only with a JSON parser and never string-interpolates them into a shell command or prompt", "States the composed budget (item cap x implement-dispatch wave cap \u2014 internal 3\u20135 default or the work_dispatch_concurrency_cap ceiling threaded in as --wave-cap when set, #573) with the loop body's arithmetic bounding the fan-out", "Persists cap, streak, latch, and guard mode into the telemetry comment's machine-readable state block" - ] + ], + "assertions": [] }, { "id": 3,