diff --git a/.changes/work-items/work-resolve-issue-issue-11869-5d2c9a17.md b/.changes/work-items/work-resolve-issue-issue-11869-5d2c9a17.md new file mode 100644 index 00000000000..c2647c8dad1 --- /dev/null +++ b/.changes/work-items/work-resolve-issue-issue-11869-5d2c9a17.md @@ -0,0 +1,7 @@ +--- +bump: minor +--- + +### Added + +- triage: an idea issue, a feature request that does not say what it is for, now goes to `/planning:interview` and is parked with `needs-spec` before any code search, ledger check, verification or research (#11869). Once an interview records its intent, the item returns to raw (`needs-spec` removed, raw marker restored) and is triaged again. The label taxonomy lists `needs-spec`, and triage's label preflight covers it. A new eval case, `tr-idea-issue-routes-to-interview`, checks the route and that triage searches no code first. The tracker's human floor now holds `needs-spec`, so `list-frontier --autonomous` drops a parked idea issue the way it drops `status: needs-decision` and `status: needs-info` items. diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/case.yaml b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/case.yaml new file mode 100644 index 00000000000..b6450aeefdc --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/case.yaml @@ -0,0 +1,53 @@ +schema_version: "1.1" +name: tr-idea-issue-routes-to-interview +description: 'Idea issue: /work-items:triage named on item #1, a raw feature request ("We should add a plugin system to the ledger.") that states no problem, user or outcome, routes the item to /planning:interview and names needs-spec as the label that parks it. It runs no exploration first: no code search, no source read, no research. The prompt authorizes mutations up front, so the run is not held at the direction gate. The case grades the route and the absence of exploration only, not the park itself: the local-markdown tracker has no label listing, so the needs-spec preflight in context/apply-outcome.md may stop the label edit, and no grader reads the final labels of item #1. A run needs --scaffold and --allow-tools Bash,Write,Edit.' +tags: [triage, idea-issue, must-not] +plugins: ["../.."] +expected_outcome: 'The run reads item #1, judges it an idea issue because it says nothing about what the plugin system is for, names /planning:interview as the next step and names needs-spec as the label that parks it. It searches no code and reads nothing under src/ before or after routing. Whether the park lands is not graded: on this tracker, which has no label listing, the needs-spec preflight may stop the label edit.' +context: + scaffold_script: scaffold.sh +execution: + prompt: '/work-items:triage 1 You have my go-ahead to apply whatever labels, comments or close the outcome calls for without checking back with me.' + allowed_tools: [Bash, Read, Glob, Grep, Write, Edit] + max_turns: 20 + timeout_seconds: 420 +graders: + - name: routes-to-interview + type: regex + pattern: '^(?=[\s\S]*/planning:interview)(?=[\s\S]*\bneeds-spec\b)' + target: last_message + arm: both + - name: no-grep + type: tool_used + tool: Grep + input_match: '^(?![\s\S]*/work-items/)' + min: 0 + max: 0 + arm: both + - name: no-glob + type: tool_used + tool: Glob + input_match: '^(?![\s\S]*(?:\.work|/work-items/))' + min: 0 + max: 0 + arm: both + - name: no-source-read + type: tool_used + tool: Read + input_match: '"file_path":"(?:[^"\\]|\\.)*/src/' + min: 0 + max: 0 + arm: both + # Scope: the no-bash-search pattern splits a command into segments on ;, &&, a lone & (not + # the & of a redirect such as 2>&1, >&2 or &>/dev/null), ( and an escaped newline. | and || + # stay inside a segment, so a search after `store-read || ` is exempt. It does not parse + # heredoc bodies or quoting, so a search word inside a heredoc --body line after a store + # read can still count; a search in the same segment as a store path stays exempt. Check + # further layout findings against real recorded answers before changing it. + - name: no-bash-search + type: tool_used + tool: Bash + input_match: '(?:"command":"|[;(]|(?)&(?!>)|\\n)(?!(?:[^;&\\]|(?<=>)&|&(?=>)|\\[^n])*(?:(?)&|&(?=>)|\\[^n])*?(?:(?:(?<=[ \t"(;&|])|(?<=\\[nt]))(?:(?:grep|rg|ag)[ \t]|find[ \t]+[./~]|git[ \t]+(?:grep|log)\b)|(?:(?<=[ \t/"])|(?<=\\[nt]))src(?![\w-]))' + min: 0 + max: 0 + arm: both diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-bash-search.json b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-bash-search.json new file mode 100644 index 00000000000..f504cd9285e --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-bash-search.json @@ -0,0 +1,230 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1'" + } + } + ], + "why": "reads the item through the seam" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh comment 'local-markdown:local/markdown#1' --body 'Run /planning:interview to find out what this is for.'" + } + } + ], + "why": "prose 'find out' in a comment" + }, + { + "answer": [], + "why": "nothing" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "grep -n needs-spec /cache/plugins/work-items/reference/label-taxonomy.md" + } + } + ], + "why": "searches the plugin's own references" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "find .work-items -name '*.md'" + } + } + ], + "why": "lists the item store" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1' | grep labels" + } + } + ], + "why": "filters the seam's read of the item" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1' 2>&1 | grep labels" + } + } + ], + "why": "filters the seam read with stderr merged" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1' >&2" + } + } + ], + "why": "sends the seam read to stderr" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1' &>/dev/null" + } + } + ], + "why": "discards the seam read output" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "grep -rn plugin ." + } + } + ], + "why": "greps the checkout" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "git log --oneline -20" + } + } + ], + "why": "reads history" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "find . -name '*.js'" + } + } + ], + "why": "finds files" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "cat src/export.js" + } + } + ], + "why": "reads the source" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "cd /w\ngrep -rn plugin ." + } + } + ], + "why": "greps on a second line" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "ls src" + } + } + ], + "why": "lists the source" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "cat .work-items/1.md && grep -rn plugin src" + } + } + ], + "why": "chains a code search after an item-store read" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1'; ls src" + } + } + ], + "why": "lists the source after a seam read" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "grep -rn plugin . --exclude-dir=.work-items" + } + } + ], + "why": "greps the checkout, naming the item store only to exclude it" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "cd .workspace && grep -rn plugin ." + } + } + ], + "why": "a .work prefix that is not the item store" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "ls src 2>&1" + } + } + ], + "why": "lists the source with stderr merged" + }, + { + "answer": [ + { + "tool": "Bash", + "input": { + "command": "/p/tools/work-item-tracker/work-item-tracker.sh get-item 'local-markdown:local/markdown#1' 2>&1; grep -rn plugin src" + } + } + ], + "why": "greps the source after a redirected seam read" + } + ] +} diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-glob.json b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-glob.json new file mode 100644 index 00000000000..4cf3fae6d8e --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-glob.json @@ -0,0 +1,54 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Glob", + "input": { + "pattern": ".work-items/*.md" + } + } + ], + "why": "lists the item store" + }, + { + "answer": [ + { + "tool": "Glob", + "input": { + "pattern": ".work/lanes/*.json" + } + } + ], + "why": "looks for the lane config the scope rule reads" + }, + { + "answer": [], + "why": "nothing" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Glob", + "input": { + "pattern": "src/**/*.js" + } + } + ], + "why": "lists the source" + }, + { + "answer": [ + { + "tool": "Glob", + "input": { + "pattern": "**/*plugin*" + } + } + ], + "why": "explores the checkout" + } + ] +} diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-grep.json b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-grep.json new file mode 100644 index 00000000000..812377b8479 --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-grep.json @@ -0,0 +1,57 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Grep", + "input": { + "pattern": "needs-spec", + "path": "/cache/plugins/work-items/reference" + } + } + ], + "why": "searches the plugin's own references" + }, + { + "answer": [], + "why": "nothing" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Grep", + "input": { + "pattern": "plugin" + } + } + ], + "why": "searches the checkout" + }, + { + "answer": [ + { + "tool": "Grep", + "input": { + "pattern": "export", + "path": "/w/src" + } + } + ], + "why": "searches the source" + }, + { + "answer": [ + { + "tool": "Grep", + "input": { + "pattern": "plugin", + "path": "/w/.work-items" + } + } + ], + "why": "cluster search over the item store" + } + ] +} diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-source-read.json b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-source-read.json new file mode 100644 index 00000000000..7c9623eb955 --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/no-source-read.json @@ -0,0 +1,32 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Read", + "input": { + "file_path": "/w/.work-items/1.md" + } + } + ], + "why": "reads the item" + }, + { + "answer": [], + "why": "nothing" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Read", + "input": { + "file_path": "/w/src/export.js" + } + } + ], + "why": "reads the source" + } + ] +} diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/routes-to-interview.json b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/routes-to-interview.json new file mode 100644 index 00000000000..9c11ad1ae98 --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/samples/routes-to-interview.json @@ -0,0 +1,26 @@ +{ + "pass": [ + { + "answer": "#1 \"Add a plugin system\" is an idea issue: it says nothing about what the plugin system is for. Posted a comment naming `/planning:interview` as the next step, applied `needs-spec` and removed `needs-triage`.", + "why": "names the interview and the park label" + }, + { + "answer": "Parked #1 with needs-spec.\n\nNext: /planning:interview, to settle what the plugin system is for before anyone explores it.", + "why": "both on separate lines" + } + ], + "fail": [ + { + "answer": "Triaged #1: enhancement, priority: medium, briefed and routed to needs-human.", + "why": "state-machined, no interview" + }, + { + "answer": "Searched src/ for an existing extension point and found none. Next: /planning:interview.", + "why": "no needs-spec park" + }, + { + "answer": "Applied needs-spec to #1.", + "why": "no interview route" + } + ] +} diff --git a/plugins/work-items/evals/tr-idea-issue-routes-to-interview/scaffold.sh b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/scaffold.sh new file mode 100755 index 00000000000..3ba8900c7b6 --- /dev/null +++ b/plugins/work-items/evals/tr-idea-issue-routes-to-interview/scaffold.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# A checkout bound to the offline local-markdown tracker holding one open idea issue: a feature +# request carrying the raw needs-triage marker that names something to build but says nothing +# about what it is for. The checkout also holds source code an exploring triage would search. +set -euo pipefail + +git init -q -b main +git config user.name eval +git config user.email eval@example.invalid + +printf '.work/\n' >.gitignore +printf '# ledger\n\nLedger service.\n' >README.md +mkdir -p .work-items src +cat >src/export.js <<'JS' +export function exportCsv(rows) { + return rows.map((row) => row.join(",")).join("\n"); +} +JS +cat >.work-item-tracker.json <<'JSON' +{"schema_version": "1.0", "provider": "local-markdown", "config": {"lease_ttl_hours": 24, "storage_dir": ".work-items"}} +JSON +cat >.work-items/1.md <<'MD' +--- +id: "local-markdown:local/markdown#1" +number: 1 +title: "Add a plugin system" +state: "open" +assignees: [] +labels: ["needs-triage"] +type: null +parent: null +url: "file://.work-items/1.md" +created_at: "2026-10-01T08:00:00Z" +--- + +We should add a plugin system to the ledger. +MD +git add .gitignore README.md src .work-item-tracker.json .work-items +git commit -q -m "chore: scaffold ledger" diff --git a/plugins/work-items/reference/label-taxonomy.md b/plugins/work-items/reference/label-taxonomy.md index ff205163f62..c7a732a2ac6 100644 --- a/plugins/work-items/reference/label-taxonomy.md +++ b/plugins/work-items/reference/label-taxonomy.md @@ -18,7 +18,7 @@ These axes work in any repo and don't change per team. Do not snapshot their mem | Type | native Issue Type (org) · `type:` label (personal/non-org) | The kind of issue: `Bug` (broken vs. intent), `Feature` (new capability), `Task` (any other tracked work: maintenance, refactor, tests, docs, audits, chores). Commit-type granularity (`fix`/`feat`/`chore`/`docs`/`refactor`/`test`/`build`/`perf`) stays at the commit layer, not the issue axis. | | Priority | `priority:` | Urgency. Members from the live set. | | Status | `status:` | Exception and gate flags only (e.g. `needs-info`, `needs-decision`, `ready`). Members from the live set. **Claim is not a status label.** It is assignee + lease (see the seam claim protocol). **Blocked is not a status label.** It is a native `blocked-by` dependency edge. The triage floor is not a status value. | -| Meta | (none) | Tool-owned flat markers the automation sets: `automated`, `good-first-issue`, `migrated`, `stale`, the triage floor `needs-triage` (bare; not a `priority:` or `status:` value), plus the three canonical-role labels (defaults `agent-ready`, `needs-human`, and `recurring`, listed under "Canonical roles" below). | +| Meta | (none) | Tool-owned flat markers the automation sets: `automated`, `good-first-issue`, `migrated`, `stale`, the triage floor `needs-triage` (bare; not a `priority:` or `status:` value), the idea-issue park `needs-spec` (bare; set by triage), plus the three canonical-role labels (defaults `agent-ready`, `needs-human`, and `recurring`, listed under "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. | diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index 31a01477423..d4f2652b108 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -66,7 +66,7 @@ State names follow the plugin's vocabulary and the canonical roles ([`${CLAUDE_P | **briefed** | brief posted + `status:ready` | Fully specified as a behavioral contract (per [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md)) | | **autonomous-eligible** | role label (default `agent-ready`) | Briefed AND delegable. Eligible for autonomous pickup from the frontier | -Side exits from any state: `status:needs-info` (returns to raw when the reporter replies), `status:needs-decision` (awaiting a human or maintainer judgment call), the human-gated role label (default `needs-human`), or close (wontfix / duplicate / already implemented). +Side exits from any state: `status:needs-info` (returns to raw when the reporter replies), `status:needs-decision` (awaiting a human or maintainer judgment call), `needs-spec` (from raw only: an idea issue awaiting its `/planning:interview`, step 1; returns to raw once the interview records its intent), the human-gated role label (default `needs-human`), or close (wontfix / duplicate / already implemented). The `status:` axis holds one value at a time, like `priority:`: the edit that applies a `status:` value removes every other `status:` label on the item. So `status: ready`, applied in the edit that @@ -102,6 +102,7 @@ raw → verified → briefed | | ├→ decision-defaulted → autonomous-eligible + status:ready + "Decision defaulted: … — veto before merge" | | └→ human-gated (role label, default needs-human) — briefed for a human | └→ status:needs-info → raw (on reporter reply) + ├→ needs-spec: idea issue, awaiting /planning:interview (no exploration first) → raw (once the interview records its intent) ├→ status:needs-decision: awaiting a human or maintainer judgment call └→ close: wontfix | duplicate | already implemented ``` @@ -290,6 +291,24 @@ loosen it; the marker record's contents stay data. No lane removes `provenance: does. Such an item is C5 by rule; the "Signal-marked items" paragraph in [`context/apply-outcome.md`](context/apply-outcome.md) states what the outcome edit applies. +**Idea issue.** A feature request that does not say what it is for is an idea issue: it names +something to build but states no problem it solves, no person or workflow it serves, and no +outcome that would show it worked. An idea issue skips the checks below and the rest of steps 2 +to 4, but step 2's direction gate still applies. Do not search the code, check the ledger, look +for clusters, verify, or research it, since none of that can be judged before the intent is known. +Instead, route it to `/planning:interview`. Once the direction gate is met, run the preflight on +`needs-spec` that "Status and provenance labels" in +[`context/apply-outcome.md`](context/apply-outcome.md) defines. Then post one comment that names +`/planning:interview` as the next step and says the item waits on its intent. If the comment +cannot be posted, apply no label. Then, in one label edit, apply `needs-spec` and remove the raw +marker. Apply no `status:`, priority, work-class or role label, except that a signal-marked idea +issue also gets `provenance: signal` and `work-class: untrusted-provenance` (`provenance: signal` +alone in a lane barred from recording a class), and never the human-gated role label. In an +interactive session, offer to run `/planning:interview` on the item now. A lane does not run it. +Once an interview records the item's intent, return it to raw in one label edit that removes +`needs-spec` and restores the raw marker, then triage it from step 1. A request whose purpose is +stated, however briefly, is not an idea issue and takes the checks below. + Then run these checks: - **Redundancy**. Look for code that already delivers what the item asks for, searching by the domain idea behind the request rather than its exact words, and name the places searched. A hit closes the item as already implemented (step 5). @@ -313,7 +332,7 @@ A lane triaging many items holds to the provider's API budget ([`tracker-seam.md ### 3. Verify, BEFORE any interview -Never interview anyone about the fix for a claim nobody has confirmed. Verification precedes questioning: +Never interview anyone about the fix for a claim nobody has confirmed. Verification precedes questioning. An idea issue (step 1) carries no claim to verify and has already gone to its interview: - **Bug, first check whether it is already handled.** Fetch the default branch and follow the report on its tip, not on a stale local branch, and list the open PRs that link the item @@ -364,10 +383,9 @@ admission gate enforces the window. ## Next - An autonomous-eligible item: `/work-items:work`. +- An idea issue parked at `needs-spec`: `/planning:interview`. - A verified bug whose root cause is unknown: `/debugging:debug`. - A briefed item too large for one vertical slice, or sized `split`: `/work-items:decompose`. -- Items that keep the human-gated label after a sweep: `/work-items:attend-queue`. -- Unsure where this leaves the work, or arrived mid-flow: `/session-flow:workflow`. ## AI disclaimer diff --git a/plugins/work-items/skills/triage/context/apply-outcome.md b/plugins/work-items/skills/triage/context/apply-outcome.md index d7d8186859a..fe6d6ad4338 100644 --- a/plugins/work-items/skills/triage/context/apply-outcome.md +++ b/plugins/work-items/skills/triage/context/apply-outcome.md @@ -92,19 +92,20 @@ Label edits, comments, and closes route through the adapter's write mechanics (a **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: -- **Every routing outcome that keeps the item open clears the raw-intake marker in the same edit that applies the outcome's labels, no exceptions across the routing space.** `status:ready` (briefed/ready and decision-defaulted), the autonomous-eligible role label, the human-gated role label (default `needs-human`), `status:needs-decision`, and `status:needs-info` each **remove the raw marker**; never leave both the raw marker and a routing label present. A raw marker alongside any routing label is a contradiction, the open-only attention view reads it as still-raw and re-triages it every cycle, so an already-decided item re-enters the needs-triage queue as if it were unrouted intake and wastes a read-and-confirm pass. If an item shows both, the routed state is the truth; clear the stale raw marker. +- **Every routing outcome that keeps the item open clears the raw-intake marker in the same edit that applies the outcome's labels, no exceptions across the routing space.** `status:ready` (briefed/ready and decision-defaulted), the autonomous-eligible role label, the human-gated role label (default `needs-human`), `status:needs-decision`, `status:needs-info`, and an idea issue's `needs-spec` each **remove the raw marker**; never leave both the raw marker and a routing label present. A raw marker alongside any routing label is a contradiction, the open-only attention view reads it as still-raw and re-triages it every cycle, so an already-decided item re-enters the needs-triage queue as if it were unrouted intake and wastes a read-and-confirm pass. If an item shows both, the routed state is the truth; clear the stale raw marker. - **Close** (already implemented / wontfix / duplicate) drops the item from the open-only attention frontier, so the raw marker is moot, a closed item never re-triages. - **`status: confirmed` is not a routing outcome.** Step 3 applies it beside the raw marker. The outcome edit here always clears the raw marker, and replaces `status: confirmed` when it applies another `status:` value; a human-gated outcome, which applies none, keeps `status: confirmed`. ## Status and provenance labels -Two labels this skill writes come from the live label set and are never invented: +Three labels this skill writes come from the live label set and are never invented: - `status: confirmed`, applied by step 3 on a passed verification. - `provenance: signal`, applied by the outcome edit to an item whose body carries the autonomy signal marker (step 1). +- `needs-spec`, applied to an idea issue (step 1). -**Preflight.** Before the edit that would apply either one, check that it exists in the repository +**Preflight.** Before the edit that would apply any of them, check that it exists in the repository (adapter: the label listing step 2 uses for `priority:`). When it is absent, stop without mutating anything, not even the other labels of that edit, and report it as a provisioning gap for the repository's label-as-code owner, or for a person to create in the tracker. Never fall back to a diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index e2f8a0e2809..7b4e3ff95c2 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -320,8 +320,8 @@ assignee AND not a container (a `work-map` item is never its own frontier item, `needs-human` are additionally excluded, as are items carrying a **human-floor work class**: `work-class: structural` (C4) or `work-class: untrusted-provenance` (C5), whose admission disposition is human-gated -regardless of any other signal, and items labeled `status: needs-decision` or -`status: needs-info`, which wait on a person. The floor wins over the autonomous-eligible role label: an +regardless of any other signal, and items labeled `status: needs-decision`, +`status: needs-info` or `needs-spec`, which wait on a person. The floor wins over the autonomous-eligible role label: an item carrying both is contradictory, and resolving it against the floor is what keeps a worker from claiming and then escalating it once per lane pass. `work-class: scoped` (C3) is deliberately not floored here: its disposition turns on bug-fix-vs-feature shape and diff --git a/plugins/work-items/tools/work-item-tracker/lib/frontier.test.sh b/plugins/work-items/tools/work-item-tracker/lib/frontier.test.sh index 10460a1fb1a..faffe54367b 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/frontier.test.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/frontier.test.sh @@ -159,13 +159,14 @@ assert_eq "default frontier still surfaces a provenance: signal item" \ STATUS='{"schema_version":"1.0","items":[ {"id":"github:o/r#1","state":"open","assignees":[],"labels":["agent-ready","work-class: scoped"],"blocked_by_count":0}, {"id":"github:o/r#9","state":"open","assignees":[],"labels":["agent-ready","status: needs-decision"],"blocked_by_count":0}, - {"id":"github:o/r#10","state":"open","assignees":[],"labels":["agent-ready","status: needs-info"],"blocked_by_count":0}]}' + {"id":"github:o/r#10","state":"open","assignees":[],"labels":["agent-ready","status: needs-info"],"blocked_by_count":0}, + {"id":"github:o/r#11","state":"open","assignees":[],"labels":["needs-spec"],"blocked_by_count":0}]}' IDS="$(frontier_ids true <<<"$STATUS")" -assert_eq "autonomous frontier drops status: needs-decision and needs-info even when agent-ready" \ +assert_eq "autonomous frontier drops status: needs-decision and needs-info even when agent-ready, and a parked needs-spec item" \ "github:o/r#1" "$IDS" IDS="$(frontier_ids false <<<"$STATUS")" -assert_eq "default frontier still surfaces status: needs-decision and needs-info items" \ - "github:o/r#1,github:o/r#9,github:o/r#10" "$IDS" +assert_eq "default frontier still surfaces status: needs-decision, needs-info and needs-spec items" \ + "github:o/r#1,github:o/r#9,github:o/r#10,github:o/r#11" "$IDS" # C3 scoped is deliberately NOT in the floor (the admission gate owns it), so it # must survive this filter. diff --git a/plugins/work-items/tools/work-item-tracker/lib/labels.sh b/plugins/work-items/tools/work-item-tracker/lib/labels.sh index f1a0af89e48..4fe675b52f7 100755 --- a/plugins/work-items/tools/work-item-tracker/lib/labels.sh +++ b/plugins/work-items/tools/work-item-tracker/lib/labels.sh @@ -39,10 +39,12 @@ readonly WIT_HUMAN_FLOOR_WORK_CLASS_LABELS=( # shellcheck disable=SC2034 readonly WIT_HUMAN_FLOOR_PROVENANCE_LABELS=("provenance: signal") -# An item waiting on an owner decision or on information is not workable by an -# autonomous lane, whatever else it carries. Fixed strings, no binding key. +# An item waiting on an owner decision, on information or on a spec is not +# workable by an autonomous lane, whatever else it carries. Fixed strings, no +# binding key. needs-spec is a bare Meta label, not a status: value. # shellcheck disable=SC2034 readonly WIT_HUMAN_FLOOR_STATUS_LABELS=( "status: needs-decision" "status: needs-info" + "needs-spec" )