From eb3b984cd25336ebec8df6399ca7cc60b1b72e22 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:46:41 -0500 Subject: [PATCH 1/7] feat(work-items): read issue-body skeletons from the org issue-body contract track add, decompose, the agent brief and bugs:write now take their issue-body headings from the issue-body contract the standards sync delivers into each plugin's reference/ directory, the same record the org issue forms are generated from. triage checks an issue body against the contract with scripts/check-body.mjs, which accepts a field under a ## or ### heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- ...eat-issue-body-contract-skills-f098795e.md | 15 +++ ...eat-issue-body-contract-skills-f3b63720.md | 15 +++ ...eat-issue-body-contract-skills-4c8a6d60.md | 15 +++ plugins/bugs/skills/write/context/template.md | 35 ++++--- .../skills/tidy/reference/scope-budget.md | 24 ++--- plugins/work-items/reference/agent-brief.md | 32 ++----- .../work-items/reference/dogfood-filing.md | 6 +- plugins/work-items/skills/decompose/SKILL.md | 2 +- .../work-items/skills/track/actions/add.md | 20 +--- plugins/work-items/skills/triage/SKILL.md | 12 +++ .../skills/triage/scripts/check-body.mjs | 96 +++++++++++++++++++ .../skills/triage/scripts/check-body.test.mjs | 64 +++++++++++++ 12 files changed, 258 insertions(+), 78 deletions(-) create mode 100644 .changes/bugs/feat-issue-body-contract-skills-f098795e.md create mode 100644 .changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md create mode 100644 .changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md create mode 100755 plugins/work-items/skills/triage/scripts/check-body.mjs create mode 100644 plugins/work-items/skills/triage/scripts/check-body.test.mjs diff --git a/.changes/bugs/feat-issue-body-contract-skills-f098795e.md b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md new file mode 100644 index 00000000000..65e186c0dbc --- /dev/null +++ b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md @@ -0,0 +1,15 @@ +--- +bump: minor +--- + +### Added + +- read issue-body skeletons from the org issue-body contract + + track add, decompose, the agent brief and bugs:write now take their + issue-body headings from the issue-body contract the standards sync + delivers into each plugin's reference/ directory, the same record the + org issue forms are generated from. triage checks an issue body against + the contract with scripts/check-body.mjs, which accepts a field under a + ## or ### heading and treats a form's skipped answer as empty. The tidy + deferred-item body follows the task type. diff --git a/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md b/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md new file mode 100644 index 00000000000..65e186c0dbc --- /dev/null +++ b/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md @@ -0,0 +1,15 @@ +--- +bump: minor +--- + +### Added + +- read issue-body skeletons from the org issue-body contract + + track add, decompose, the agent brief and bugs:write now take their + issue-body headings from the issue-body contract the standards sync + delivers into each plugin's reference/ directory, the same record the + org issue forms are generated from. triage checks an issue body against + the contract with scripts/check-body.mjs, which accepts a field under a + ## or ### heading and treats a form's skipped answer as empty. The tidy + deferred-item body follows the task type. diff --git a/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md new file mode 100644 index 00000000000..65e186c0dbc --- /dev/null +++ b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md @@ -0,0 +1,15 @@ +--- +bump: minor +--- + +### Added + +- read issue-body skeletons from the org issue-body contract + + track add, decompose, the agent brief and bugs:write now take their + issue-body headings from the issue-body contract the standards sync + delivers into each plugin's reference/ directory, the same record the + org issue forms are generated from. triage checks an issue body against + the contract with scripts/check-body.mjs, which accepts a field under a + ## or ### heading and treats a form's skipped answer as empty. The tidy + deferred-item body follows the task type. diff --git a/plugins/bugs/skills/write/context/template.md b/plugins/bugs/skills/write/context/template.md index 3fcb9b453d7..29676efdf30 100644 --- a/plugins/bugs/skills/write/context/template.md +++ b/plugins/bugs/skills/write/context/template.md @@ -1,36 +1,33 @@ # `/bugs:write` template -Loaded on demand by `/bugs:write`. Contains: the full Markdown template (5 fields), the `--file` frontmatter, and the "No bug confirmed" form. +Loaded on demand by `/bugs:write`. Contains: the full Markdown template (5 fields, laid out by the issue-body contract), the `--file` frontmatter, and the "No bug confirmed" form. ## Full template +The body sections come from the org issue-body contract, so a filed report matches the org bug form +and passes the intake heading check. Read the `bug` type of +[`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json) +and write one `## ` section per entry of its `fields`, in that order, never a heading the +contract does not name except `## Notes`. Fill each from the matching part of the five fields: + +- Steps to reproduce: a numbered list. +- Expected and actual behavior: one section each. +- Severity: one of the field's `options`, alone; Severity justification: the one sentence that + justifies it (blast radius, blocking factor, data-integrity impact). +- Suggested fix location: `` `` `` ``, no patch. +- A field the report cannot back, required or not, gets the placeholder. + Default emission (stdout), no frontmatter: ```markdown # Bug: -**Severity**: <low | medium | high | critical> -**Suggested fix location**: `<file path>` `<function or class name>` (no patch) **Reported by**: <reporter identity, or session ID> **Reported at**: <ISO-8601 timestamp> -## Steps to reproduce - -1. <step> -2. <step> -3. <step> - -## Expected behavior - -<what should happen> - -## Actual behavior - -<what does happen> - -## Severity justification +## <each contract field heading, in contract order> -<one sentence: why this severity. Cite blast radius / blocking factor / data-integrity impact> +<its value> ## Notes diff --git a/plugins/code-tidying/skills/tidy/reference/scope-budget.md b/plugins/code-tidying/skills/tidy/reference/scope-budget.md index 74b08610356..ee7f5e18e60 100644 --- a/plugins/code-tidying/skills/tidy/reference/scope-budget.md +++ b/plugins/code-tidying/skills/tidy/reference/scope-budget.md @@ -39,27 +39,21 @@ Read the house style (`<plugin-root>/reference/issue-house-style.md`) before fil ### Body format (use as-is, fill in the angle-bracket placeholders) +The sections follow the `task` type of the org issue-body contract, which `/work-items:track add` +reads from its plugin's `reference/issue-body-contract.json`: Context, Proposed work and Acceptance +criteria are its required fields, so the item passes intake's body check. + ```markdown ## Context Deferred from tidy run on `<branch-name>` (anchor: `<anchor-sha>`). The hunt found this candidate but the PR scope budget's hard cap was reached before it could be included. -## Tidying type - -<one of the named tidyings from reference/tidyings.md, e.g., "Beck #5: Reading Order"> - -## Files - -- `<path/to/file1.ext>` (<estimated LOC delta>) -- `<path/to/file2.ext>` (<estimated LOC delta>) - -## Estimated scope - -<estimate>: ~<N> LOC across <M> files. Should fit within the scope budget's target as a future tidy run. - -## Lane +## Proposed work -`<lane-name>` +- Tidying: <one of the named tidyings from reference/tidyings.md, e.g., "Beck #5: Reading Order"> +- Files: `<path/to/file1.ext>` (<estimated LOC delta>), `<path/to/file2.ext>` (<estimated LOC delta>) +- Estimated scope: ~<N> LOC across <M> files; should fit the scope budget's target as a future tidy run. +- Lane: `<lane-name>` ## Acceptance criteria diff --git a/plugins/work-items/reference/agent-brief.md b/plugins/work-items/reference/agent-brief.md index bb5384933e9..8577358f2a6 100644 --- a/plugins/work-items/reference/agent-brief.md +++ b/plugins/work-items/reference/agent-brief.md @@ -28,32 +28,20 @@ Each pair below comes from one of two changes, a retry backoff cap and a CSV exp ## Template +The brief's fields are the `agent-ready` type of the issue-body contract +([`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json)), +the record the org agent-ready issue form is generated from. Write one `### <heading>` per entry of +its `fields`, in contract order, under the `## Agent Brief` heading; a form-filed item already +carries them as `###` sections. The type is the issue's own (native Issue Type on org repos, `type:` +label on personal / non-org repos), never a brief field. Each field's `description` says what it +holds; the rules table above says how to write the four that go stale. Shape: + ```markdown ## Agent Brief -**Type:** Bug / Feature / Task (the issue's type: native Issue Type on org repos, `type:` label on personal / non-org repos) -**Summary:** <the change, in one line> - -**Root cause:** <bugs only: `unknown`, or the cause in one line, e.g. `known: stale cache key in RetryPolicy`> - -**Current behavior:** -<What the code does today. A bug: the faulty result. A feature: the existing behavior it extends.> - -**Desired behavior:** -<What the code does once this lands, including how it handles edge cases and failures.> - -**Key interfaces:** -- `TypeName`: <the change and the reason for it> -- `FunctionName()` return type: <today's value vs the intended one> -- Config shape: <settings added or changed> - -**Acceptance criteria:** -- [ ] <a check with a yes-or-no result> -- [ ] <a check with a yes-or-no result> +### <each agent-ready field heading, in contract order> -**Out of scope:** -- <something this change must not touch> -- <a nearby feature that stays as it is> +<its value; `- [ ]` checks under Acceptance criteria, bullets under Key interfaces and Out of scope> ``` ## When to use diff --git a/plugins/work-items/reference/dogfood-filing.md b/plugins/work-items/reference/dogfood-filing.md index 1fc0360c423..d8e42f23f2e 100644 --- a/plugins/work-items/reference/dogfood-filing.md +++ b/plugins/work-items/reference/dogfood-filing.md @@ -47,9 +47,9 @@ Four beats, in order. The two **mechanical** beats reuse existing machinery verb filer records what it observed, not a verified diagnosis. 3. **File with the fixed shape (mechanical).** File through the canonical `track add` path, which - owns the body template (Context / Proposed work / Acceptance criteria / References / Metadata) and - the argv-safe `create-item` write ([`../skills/track/actions/add.md`](../skills/track/actions/add.md) - "Build body", "Create the item"). The shape is not restated here. `track add` is its source of + builds the body from the issue-body contract ([`issue-body-contract.json`](issue-body-contract.json)) + and owns the argv-safe `create-item` write ([`../skills/track/actions/add.md`](../skills/track/actions/add.md) + "Build body", "Create the item"). The shape is not restated here: the contract is its source of truth, so a change to the template lands in one place. The item **title** follows the convention in [`issue-conventions.md`](issue-conventions.md). diff --git a/plugins/work-items/skills/decompose/SKILL.md b/plugins/work-items/skills/decompose/SKILL.md index f63bd036108..1c594d33643 100644 --- a/plugins/work-items/skills/decompose/SKILL.md +++ b/plugins/work-items/skills/decompose/SKILL.md @@ -217,7 +217,7 @@ Run each call as its own plain Bash command: no `$(...)`, no `git`, and no `&&` - **As of**: 2026-10-11 - **Recheck trigger**: a Claude Code release in which the pre-#6929 snippet (tracker path fallback through `$(git rev-parse --show-toplevel)`, body through `"$(cat "$BODY_FILE")"`) runs from a worktree-isolated session. -Every item body uses the one structure below. It is the agent-brief template ([`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md)) laid out as sections: `## Outcome` carries that template's Summary and Current/Desired behavior, `## Done when` carries its Acceptance criteria, and `## Key interfaces` and `## Out of scope` are its fields of the same names, so an AFK item needs no second `## Agent Brief` block. When the source is a PR (an item with attached code), use that reference's PR-variant (current-behavior-of-the-diff, finish-what-exists); do not replace the bug/feature template for ordinary items. Body structure: +Every item body uses the one structure below. It is the agent-brief template ([`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md)) laid out as sections: `## Outcome` carries that template's Summary and Current/Desired behavior, `## Done when` carries its Acceptance criteria, and `## Key interfaces` and `## Out of scope` are its fields of the same names, so an AFK item needs no second `## Agent Brief` block. When the source is a PR (an item with attached code), use that reference's PR-variant (current-behavior-of-the-diff, finish-what-exists); do not replace the bug/feature template for ordinary items. The required sections (Outcome, Key interfaces, Done when, Out of scope) are the `decomposed-item` type of the issue-body contract ([`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json)): take their headings and order from its `fields`, and when the contract and the structure below disagree, the contract wins. Body structure: ```markdown ## Parent diff --git a/plugins/work-items/skills/track/actions/add.md b/plugins/work-items/skills/track/actions/add.md index 3f68a569751..b8262926d9b 100644 --- a/plugins/work-items/skills/track/actions/add.md +++ b/plugins/work-items/skills/track/actions/add.md @@ -5,7 +5,7 @@ Create a new work item with labels from the taxonomy. **Defaults applied by this action:** - **Priority.** When the `--priority` flag is absent, set no priority, on the `Priority` field or as a `priority:` label. Urgency is assigned at triage, not at filing. Apply the bare floor label `needs-triage` when that exact name exists in the live set ([`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md)). Do not pass it as `--priority`, and do not substitute a low priority or a prefixed `needs-triage` spelling. Omit the floor when the live set has no `needs-triage` label. -- **Body template.** When `--body` is not provided, fall back to the default skeleton: a `## Context` paragraph (what observation surfaced this item, what's the cost of leaving it), a `## Proposed work` bullet list (concrete next actions), `## Acceptance criteria` (one verifiable assertion per bullet), and `## References` (cross-references to rules, files, prior PRs, or external docs). The concrete body the workflow builds is detailed in step "Build body" below. +- **Body template.** When `--body-file` is not provided, the body's sections come from the issue-body contract ([`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json)), the record the org issue forms are generated from: the contract type matching the resolved issue type, or `agent-ready` with `--agent-ready`. The concrete body the workflow builds is detailed in step "Build body" below. - **Label taxonomy.** Labels are validated against the structure documented in [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md). - **Title shape.** The item title follows the convention in [`${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md`](${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md). @@ -52,25 +52,9 @@ Create a new work item with labels from the taxonomy. 1. **Build labels list** `{labels}` (comma-separated for the seam) from the remaining flags. With no `--priority` and no `--agent-ready`, include no `priority:` label and include the bare `needs-triage` floor when the live set has it. A supplied `--priority`, or `--agent-ready`, does not leave the floor on: a known tier or a complete brief is already past intake. Also start from `category:general` and replace it with a supplied `--category`. Append `--area`/`--ecosystem` labels when provided. When `--agent-ready` is set, also append the autonomous-eligible role label (default `agent-ready`) so the item is eligible for autonomous pickup. A default that the consuming repo doesn't define is omitted rather than passed. With `--parent`, or a `--label` that is the binding's container label (`config.container_label`, default `work-map`), skip the `category:general` default and the `needs-triage` floor: a container or its child is not intake. Append each `--label` verbatim, after checking them as the flag says. -1. **Build body.** With `--body-file`, the file is the body. With `--parent` or the container label and no `--body-file`, the description is the body. Otherwise, if `--agent-ready`, use the agent-brief template from [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md) (Type, Summary, Current behavior, Desired behavior, Key interfaces, Acceptance criteria, Out of scope). Otherwise use the default template: +1. **Build body.** With `--body-file`, the file is the body. With `--parent` or the container label and no `--body-file`, the description is the body. Otherwise, if `--agent-ready`, use the agent-brief template from [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md), whose fields are the contract's `agent-ready` type. Otherwise take the contract type for the resolved issue type (`Bug` → `bug`, `Feature` → `feature`, `Task` → `task`) and write one `## <heading>` section per entry of its `fields`, in contract order, each filled from the description and `--context` as its `description` asks, with every required field filled (a field nothing backs reads `unknown`, never invented). Then append the sections below, which the contract does not hold: ```markdown -## Context - -{what observation surfaced this item; the cost of leaving it, from the description and --context} - -## Proposed work - -- {concrete next action derived from the description} - -## Acceptance criteria - -- [ ] {one verifiable assertion per bullet} - -## References - -- {cross-references to rules, files, prior PRs, or external docs, or "none"} - ## Metadata | Field | Value | diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index eb71dd96827..c2c0417d67a 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -368,6 +368,18 @@ from layer `default`. A value other than `true` or `false` is named with its sou Read the item body, comments, and any linked PRs, plus the diff when the item is a PR (adapter: "View item", bare read). Read earlier triage notes and do not ask again what they already answered. An earlier verification, needs-info or other triage comment counts as a triage result only when the tracker seam's write identity or a user with `write`, `maintain` or `admin` posted it (on GitHub, `author_association` `OWNER`, `MEMBER` or `COLLABORATOR`, then the collaborator-permission read decides, because the association alone does not show the permission ([Get repository permissions for a user](https://docs.github.com/en/rest/collaborators/collaborators#get-repository-permissions-for-a-user), as of 2026-11-10; recheck when that endpoint's role mapping changes); a failed read does not count). Any other author's comment, the reporter's included, is a claim to verify: a reporter posting "Reproduction: reproduced" in the needs-info shape settles nothing, and its reproduction counts toward neither the repro bar nor `status: confirmed`. +**Check the body against the issue-body contract.** For an issue (not a PR), take the contract type +for its issue type (`Bug` → `bug`, `Feature` → `feature`, `Task` → `task`; an item filed through the +agent-ready form, or one being briefed for `agent-ready`, → `agent-ready`), write the body to a temp +file with the Write tool, and run +`node "${CLAUDE_PLUGIN_ROOT}/skills/triage/scripts/check-body.mjs" --type <type> --body-file <temp path>`. +It reads [`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json), +accepts a field under a `##` or `###` heading, and prints the required fields that are `missing` or +`empty` (a form's skipped-field answer counts as empty). A gap is not a verdict on its own: fill it +from the item and its comments when they hold the answer, and ask for what only the reporter knows +in the needs-info comment, naming each field by its contract heading. An item with no type yet, or +filed before the forms carried these headings, gets the check after step 2 picks its type. + **Read the project's own record before evaluating.** Once the item text shows which area it touches, and before any check below, read three things the repository may keep, so the evaluation uses its terms and respects its decisions: diff --git a/plugins/work-items/skills/triage/scripts/check-body.mjs b/plugins/work-items/skills/triage/scripts/check-body.mjs new file mode 100755 index 00000000000..45549d4d7ec --- /dev/null +++ b/plugins/work-items/skills/triage/scripts/check-body.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +// Checks an issue body against one type of the issue-body contract +// (plugins/work-items/reference/issue-body-contract.json, synced from melodic-software/standards). +// A field counts as present under a heading at any of the contract's `headingLevels` (`##` from a +// skill, `###` from an issue form), with text other than the contract's `emptyAnswer` before the +// next heading at that level or shallower. HTML comments and fenced code do not count. +// +// Usage: check-body.mjs --type <type> --body-file <path> [--contract <path>] +// Prints one JSON object: {type, missing: [heading...], empty: [heading...], unknownType}. +// Exit 0 when every required field is present and answered, 1 when one is not, 2 on usage error. + +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const DEFAULT_CONTRACT = path.join( + path.dirname(fileURLToPath(import.meta.url)), + "../../../reference/issue-body-contract.json", +); + +const normalize = (text) => text.trim().replace(/\s+/g, " ").toLowerCase(); + +// Lines outside fenced code, with HTML comments removed. +function proseLines(body) { + const lines = body.replace(/<!--[\s\S]*?-->/g, "").split(/\r?\n/); + let fence = null; + return lines.map((line) => { + const marker = line.match(/^ {0,3}(`{3,}|~{3,})/); + if (fence) { + if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = null; + return ""; + } + if (marker) { + fence = marker[1]; + return ""; + } + return line; + }); +} + +// Maps each normalized heading at an allowed level to the text under it. +export function sections(body, levels) { + const found = new Map(); + let open = null; + for (const line of proseLines(body)) { + const heading = line.match(/^ {0,3}(#{1,6})[ \t]+(.*?)[ \t#]*$/); + if (heading) { + const depth = heading[1].length; + if (open && depth <= open.depth) open = null; + if (levels.includes(depth)) { + open = { depth, text: [] }; + const key = normalize(heading[2]); + if (!found.has(key)) found.set(key, open.text); + } + continue; + } + open?.text.push(line); + } + return found; +} + +export function checkBody(contract, typeKey, body) { + const type = contract.types[typeKey]; + if (!type) return { type: typeKey, unknownType: true, missing: [], empty: [] }; + const found = sections(body, contract.headingLevels); + const missing = []; + const empty = []; + for (const field of type.fields.filter((f) => f.required)) { + const text = found.get(normalize(field.heading)); + if (text === undefined) missing.push(field.heading); + else { + const answer = text.join("\n").trim(); + if (answer === "" || answer === contract.emptyAnswer) empty.push(field.heading); + } + } + return { type: typeKey, unknownType: false, missing, empty }; +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + const args = process.argv.slice(2); + const flag = (name) => { + const index = args.indexOf(name); + return index === -1 ? undefined : args[index + 1]; + }; + const typeKey = flag("--type"); + const bodyFile = flag("--body-file"); + if (!typeKey || !bodyFile) { + process.stderr.write("usage: check-body.mjs --type <type> --body-file <path> [--contract <path>]\n"); + process.exit(2); + } + const contract = JSON.parse(await readFile(flag("--contract") ?? DEFAULT_CONTRACT, "utf8")); + const result = checkBody(contract, typeKey, await readFile(bodyFile, "utf8")); + process.stdout.write(`${JSON.stringify(result)}\n`); + process.exit(result.unknownType ? 2 : result.missing.length + result.empty.length > 0 ? 1 : 0); +} diff --git a/plugins/work-items/skills/triage/scripts/check-body.test.mjs b/plugins/work-items/skills/triage/scripts/check-body.test.mjs new file mode 100644 index 00000000000..870d0a02cc9 --- /dev/null +++ b/plugins/work-items/skills/triage/scripts/check-body.test.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { checkBody } from "./check-body.mjs"; + +const contract = { + headingLevels: [2, 3], + emptyAnswer: "_No response_", + types: { + task: { + fields: [ + { heading: "Context", required: true }, + { heading: "Proposed work", required: true }, + { heading: "References", required: false }, + ], + }, + }, +}; + +describe("checkBody", () => { + it("accepts `##` headings a skill writes", () => { + const body = "## Context\n\nWhy.\n\n## Proposed work\n\n- do it\n"; + assert.deepEqual(checkBody(contract, "task", body), { + type: "task", + unknownType: false, + missing: [], + empty: [], + }); + }); + + it("accepts `###` headings an issue form renders, matching heading case loosely", () => { + const body = "### context\n\nWhy.\n\n### Proposed work\n\n- do it\n\n### References\n\n_No response_\n"; + assert.deepEqual(checkBody(contract, "task", body).missing, []); + assert.deepEqual(checkBody(contract, "task", body).empty, []); + }); + + it("reports a missing required heading and ignores a missing optional one", () => { + assert.deepEqual(checkBody(contract, "task", "## Context\n\nWhy.\n").missing, ["Proposed work"]); + }); + + it("reports a form's skipped field and an empty section as empty", () => { + const body = "### Context\n\n_No response_\n\n### Proposed work\n\n### References\n\nx\n"; + assert.deepEqual(checkBody(contract, "task", body).empty, ["Context", "Proposed work"]); + }); + + it("does not count a heading at a level the contract does not accept", () => { + const body = "# Context\n\nWhy.\n\n#### Proposed work\n\n- do it\n"; + assert.deepEqual(checkBody(contract, "task", body).missing, ["Context", "Proposed work"]); + }); + + it("does not count headings inside fenced code or HTML comments", () => { + const body = "```markdown\n## Context\n\nWhy.\n```\n\n<!--\n## Proposed work\nx\n-->\n"; + assert.deepEqual(checkBody(contract, "task", body).missing, ["Context", "Proposed work"]); + }); + + it("keeps a deeper sub-heading's text inside its section", () => { + const body = "## Context\n\n#### Detail\n\nWhy.\n\n## Proposed work\n\n- x\n"; + assert.deepEqual(checkBody(contract, "task", body).empty, []); + }); + + it("flags an unknown type", () => { + assert.equal(checkBody(contract, "epic", "").unknownType, true); + }); +}); From f81e29dcbad91829b0f05d56ef144ceab42ea0b9 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:51:15 -0500 Subject: [PATCH 2/7] fix(work-items): link the contract relatively from spoke files Spoke files are read as raw bytes, so the contract link in track add's action file and the bugs:write template uses a relative path instead of ${CLAUDE_PLUGIN_ROOT}. The changelog fragments no longer start a line with a heading marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- .changes/bugs/feat-issue-body-contract-skills-f098795e.md | 4 ++-- .../code-tidying/feat-issue-body-contract-skills-f3b63720.md | 4 ++-- .../work-items/feat-issue-body-contract-skills-4c8a6d60.md | 4 ++-- plugins/bugs/skills/write/context/template.md | 2 +- plugins/work-items/skills/track/actions/add.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.changes/bugs/feat-issue-body-contract-skills-f098795e.md b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md index 65e186c0dbc..03ce6986da8 100644 --- a/.changes/bugs/feat-issue-body-contract-skills-f098795e.md +++ b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md @@ -10,6 +10,6 @@ bump: minor issue-body headings from the issue-body contract the standards sync delivers into each plugin's reference/ directory, the same record the org issue forms are generated from. triage checks an issue body against - the contract with scripts/check-body.mjs, which accepts a field under a - ## or ### heading and treats a form's skipped answer as empty. The tidy + the contract with scripts/check-body.mjs, which accepts a field under + a `##` or `###` heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. diff --git a/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md b/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md index 65e186c0dbc..03ce6986da8 100644 --- a/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md +++ b/.changes/code-tidying/feat-issue-body-contract-skills-f3b63720.md @@ -10,6 +10,6 @@ bump: minor issue-body headings from the issue-body contract the standards sync delivers into each plugin's reference/ directory, the same record the org issue forms are generated from. triage checks an issue body against - the contract with scripts/check-body.mjs, which accepts a field under a - ## or ### heading and treats a form's skipped answer as empty. The tidy + the contract with scripts/check-body.mjs, which accepts a field under + a `##` or `###` heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. diff --git a/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md index 65e186c0dbc..03ce6986da8 100644 --- a/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md +++ b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md @@ -10,6 +10,6 @@ bump: minor issue-body headings from the issue-body contract the standards sync delivers into each plugin's reference/ directory, the same record the org issue forms are generated from. triage checks an issue body against - the contract with scripts/check-body.mjs, which accepts a field under a - ## or ### heading and treats a form's skipped answer as empty. The tidy + the contract with scripts/check-body.mjs, which accepts a field under + a `##` or `###` heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. diff --git a/plugins/bugs/skills/write/context/template.md b/plugins/bugs/skills/write/context/template.md index 29676efdf30..f7c48078d0d 100644 --- a/plugins/bugs/skills/write/context/template.md +++ b/plugins/bugs/skills/write/context/template.md @@ -6,7 +6,7 @@ Loaded on demand by `/bugs:write`. Contains: the full Markdown template (5 field The body sections come from the org issue-body contract, so a filed report matches the org bug form and passes the intake heading check. Read the `bug` type of -[`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json) +[`reference/issue-body-contract.json`](../../../reference/issue-body-contract.json) and write one `## <heading>` section per entry of its `fields`, in that order, never a heading the contract does not name except `## Notes`. Fill each from the matching part of the five fields: diff --git a/plugins/work-items/skills/track/actions/add.md b/plugins/work-items/skills/track/actions/add.md index b8262926d9b..3032a0a4a12 100644 --- a/plugins/work-items/skills/track/actions/add.md +++ b/plugins/work-items/skills/track/actions/add.md @@ -5,7 +5,7 @@ Create a new work item with labels from the taxonomy. **Defaults applied by this action:** - **Priority.** When the `--priority` flag is absent, set no priority, on the `Priority` field or as a `priority:` label. Urgency is assigned at triage, not at filing. Apply the bare floor label `needs-triage` when that exact name exists in the live set ([`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md)). Do not pass it as `--priority`, and do not substitute a low priority or a prefixed `needs-triage` spelling. Omit the floor when the live set has no `needs-triage` label. -- **Body template.** When `--body-file` is not provided, the body's sections come from the issue-body contract ([`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json)), the record the org issue forms are generated from: the contract type matching the resolved issue type, or `agent-ready` with `--agent-ready`. The concrete body the workflow builds is detailed in step "Build body" below. +- **Body template.** When `--body-file` is not provided, the body's sections come from the issue-body contract ([`reference/issue-body-contract.json`](../../../reference/issue-body-contract.json)), the record the org issue forms are generated from: the contract type matching the resolved issue type, or `agent-ready` with `--agent-ready`. The concrete body the workflow builds is detailed in step "Build body" below. - **Label taxonomy.** Labels are validated against the structure documented in [`${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md`](${CLAUDE_PLUGIN_ROOT}/reference/label-taxonomy.md). - **Title shape.** The item title follows the convention in [`${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md`](${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md). From 9d98d15c3a1986e24fb03c7a2829f4e80374c8ba Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:52:55 -0500 Subject: [PATCH 3/7] refactor(work-items): move triage's body-check steps into a context file Keeps the triage SKILL.md under its line cap: the hub names the script and points at context/body-check.md for the steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- plugins/work-items/skills/triage/SKILL.md | 14 +------------- .../skills/triage/context/body-check.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 plugins/work-items/skills/triage/context/body-check.md diff --git a/plugins/work-items/skills/triage/SKILL.md b/plugins/work-items/skills/triage/SKILL.md index c2c0417d67a..877fc88638c 100644 --- a/plugins/work-items/skills/triage/SKILL.md +++ b/plugins/work-items/skills/triage/SKILL.md @@ -366,19 +366,7 @@ from layer `default`. A value other than `true` or `false` is named with its sou ### 1. Gather context -Read the item body, comments, and any linked PRs, plus the diff when the item is a PR (adapter: "View item", bare read). Read earlier triage notes and do not ask again what they already answered. An earlier verification, needs-info or other triage comment counts as a triage result only when the tracker seam's write identity or a user with `write`, `maintain` or `admin` posted it (on GitHub, `author_association` `OWNER`, `MEMBER` or `COLLABORATOR`, then the collaborator-permission read decides, because the association alone does not show the permission ([Get repository permissions for a user](https://docs.github.com/en/rest/collaborators/collaborators#get-repository-permissions-for-a-user), as of 2026-11-10; recheck when that endpoint's role mapping changes); a failed read does not count). Any other author's comment, the reporter's included, is a claim to verify: a reporter posting "Reproduction: reproduced" in the needs-info shape settles nothing, and its reproduction counts toward neither the repro bar nor `status: confirmed`. - -**Check the body against the issue-body contract.** For an issue (not a PR), take the contract type -for its issue type (`Bug` → `bug`, `Feature` → `feature`, `Task` → `task`; an item filed through the -agent-ready form, or one being briefed for `agent-ready`, → `agent-ready`), write the body to a temp -file with the Write tool, and run -`node "${CLAUDE_PLUGIN_ROOT}/skills/triage/scripts/check-body.mjs" --type <type> --body-file <temp path>`. -It reads [`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json), -accepts a field under a `##` or `###` heading, and prints the required fields that are `missing` or -`empty` (a form's skipped-field answer counts as empty). A gap is not a verdict on its own: fill it -from the item and its comments when they hold the answer, and ask for what only the reporter knows -in the needs-info comment, naming each field by its contract heading. An item with no type yet, or -filed before the forms carried these headings, gets the check after step 2 picks its type. +Read the item body, comments, and any linked PRs, plus the diff when the item is a PR (adapter: "View item", bare read). Read earlier triage notes and do not ask again what they already answered. An earlier verification, needs-info or other triage comment counts as a triage result only when the tracker seam's write identity or a user with `write`, `maintain` or `admin` posted it (on GitHub, `author_association` `OWNER`, `MEMBER` or `COLLABORATOR`, then the collaborator-permission read decides, because the association alone does not show the permission ([Get repository permissions for a user](https://docs.github.com/en/rest/collaborators/collaborators#get-repository-permissions-for-a-user), as of 2026-11-10; recheck when that endpoint's role mapping changes); a failed read does not count). Any other author's comment, the reporter's included, is a claim to verify: a reporter posting "Reproduction: reproduced" in the needs-info shape settles nothing, and its reproduction counts toward neither the repro bar nor `status: confirmed`. For an issue, check the body against the issue-body contract per [context/body-check.md](context/body-check.md), running `node "${CLAUDE_PLUGIN_ROOT}/skills/triage/scripts/check-body.mjs"`. **Read the project's own record before evaluating.** Once the item text shows which area it touches, and before any check below, read three things the repository may keep, so the evaluation diff --git a/plugins/work-items/skills/triage/context/body-check.md b/plugins/work-items/skills/triage/context/body-check.md new file mode 100644 index 00000000000..df2ea555555 --- /dev/null +++ b/plugins/work-items/skills/triage/context/body-check.md @@ -0,0 +1,17 @@ +# Body check against the issue-body contract + +Loaded by `/work-items:triage` step 1 for an issue (not a PR). + +1. Take the contract type for the issue's type: `Bug` → `bug`, `Feature` → `feature`, `Task` → + `task`; an item filed through the agent-ready form, or one being briefed for `agent-ready`, → + `agent-ready`. An item with no type yet gets the check after step 2 picks its type. +2. Write the body to a temp file with the Write tool and run the script SKILL.md names with + `--type <type> --body-file <temp path>`. It reads the plugin's + [`reference/issue-body-contract.json`](../../../reference/issue-body-contract.json), the record + the org issue forms are generated from, accepts a field under any of the contract's + `headingLevels` (`##` from a skill, `###` from a form), and prints the required fields that are + `missing` or `empty`; a form's skipped-field answer counts as empty. Exit 0 means none. +3. A gap is not a verdict on its own. Fill it from the item and its comments when they hold the + answer, and ask for what only the reporter knows in the needs-info comment, naming each field by + its contract heading. An item filed before the forms carried these headings is held to the same + fields, by content rather than heading. From 0c789afd8ba8227ce5b976d398de62e2e111e52e Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:47:18 -0500 Subject: [PATCH 4/7] feat(work-items): fan the synced issue-body contract into the plugins The standards sync delivers the contract once, to docs/contracts/issue-body/contract.json. sync-shared-copies.sh now copies a JSON canonical byte for byte (JSON has no comment syntax for a header) and generates plugins/bugs and plugins/work-items reference/issue-body-contract.json from it; the cross-plugin registry records the cluster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- ...eat-issue-body-contract-skills-f098795e.md | 9 + ...eat-issue-body-contract-skills-4c8a6d60.md | 9 + .../bugs/reference/issue-body-contract.json | 235 ++++++++++++++++++ .../reference/issue-body-contract.json | 235 ++++++++++++++++++ scripts/cross-plugin-source-registry.txt | 8 + scripts/shared-copies.txt | 2 + scripts/sync-shared-copies.sh | 8 +- scripts/sync-shared-copies.test.sh | 18 +- 8 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 plugins/bugs/reference/issue-body-contract.json create mode 100644 plugins/work-items/reference/issue-body-contract.json diff --git a/.changes/bugs/feat-issue-body-contract-skills-f098795e.md b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md index 03ce6986da8..44fce690964 100644 --- a/.changes/bugs/feat-issue-body-contract-skills-f098795e.md +++ b/.changes/bugs/feat-issue-body-contract-skills-f098795e.md @@ -13,3 +13,12 @@ bump: minor the contract with scripts/check-body.mjs, which accepts a field under a `##` or `###` heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. + +- fan the synced issue-body contract into the plugins + + The standards sync delivers the contract once, to + docs/contracts/issue-body/contract.json. sync-shared-copies.sh now + copies a JSON canonical byte for byte (JSON has no comment syntax for a + header) and generates plugins/bugs and plugins/work-items + reference/issue-body-contract.json from it; the cross-plugin registry + records the cluster. diff --git a/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md index 03ce6986da8..44fce690964 100644 --- a/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md +++ b/.changes/work-items/feat-issue-body-contract-skills-4c8a6d60.md @@ -13,3 +13,12 @@ bump: minor the contract with scripts/check-body.mjs, which accepts a field under a `##` or `###` heading and treats a form's skipped answer as empty. The tidy deferred-item body follows the task type. + +- fan the synced issue-body contract into the plugins + + The standards sync delivers the contract once, to + docs/contracts/issue-body/contract.json. sync-shared-copies.sh now + copies a JSON canonical byte for byte (JSON has no comment syntax for a + header) and generates plugins/bugs and plugins/work-items + reference/issue-body-contract.json from it; the cross-plugin registry + records the cluster. diff --git a/plugins/bugs/reference/issue-body-contract.json b/plugins/bugs/reference/issue-body-contract.json new file mode 100644 index 00000000000..93a7c625dea --- /dev/null +++ b/plugins/bugs/reference/issue-body-contract.json @@ -0,0 +1,235 @@ +{ + "schemaVersion": 1, + "headingLevels": [2, 3], + "emptyAnswer": "_No response_", + "floorLabels": ["needs-triage"], + "types": { + "bug": { + "form": "bug-report.yml", + "name": "Bug Report", + "description": "Report an unexpected problem or behavior.", + "nativeType": "Bug", + "labels": ["needs-triage"], + "fields": [ + { + "id": "steps", + "heading": "Steps to reproduce", + "kind": "textarea", + "required": true, + "description": "Minimal steps that reproduce the behavior.", + "placeholder": "1. ...\n2. ...\n3. ..." + }, + { + "id": "expected", + "heading": "Expected behavior", + "kind": "textarea", + "required": true, + "description": "What should happen?" + }, + { + "id": "actual", + "heading": "Actual behavior", + "kind": "textarea", + "required": true, + "description": "What actually happens? Include error messages, stack traces, or screenshots." + }, + { + "id": "severity", + "heading": "Severity", + "kind": "dropdown", + "required": true, + "description": "How bad is it? Say why under Severity justification.", + "options": ["low", "medium", "high", "critical"] + }, + { + "id": "severity-justification", + "heading": "Severity justification", + "kind": "textarea", + "required": true, + "description": "One sentence: the blast radius, what it blocks, or the data at risk." + }, + { + "id": "fix-location", + "heading": "Suggested fix location", + "kind": "input", + "required": false, + "description": "The file and function or class where the fix likely belongs, if known. No patch." + }, + { + "id": "environment", + "heading": "Environment", + "kind": "input", + "required": false, + "description": "OS, runtime and tool versions, and anything else relevant." + } + ] + }, + "feature": { + "form": "feature-request.yml", + "name": "Feature Request", + "description": "Propose a new feature or enhancement.", + "nativeType": "Feature", + "labels": ["needs-triage"], + "fields": [ + { + "id": "user-story", + "heading": "User story", + "kind": "textarea", + "required": true, + "description": "Who benefits and why?", + "placeholder": "As a [role], I want [capability] so that [benefit]." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "How do we know this is done? One verifiable check per line.", + "placeholder": "- [ ] Criterion 1\n- [ ] Criterion 2" + }, + { + "id": "area", + "heading": "Affected area / component", + "kind": "input", + "required": false, + "description": "Which part of the project would this affect?" + } + ] + }, + "task": { + "form": "task.yml", + "name": "Task", + "description": "A chore, maintenance task, audit, or other housekeeping item.", + "nativeType": "Task", + "labels": ["needs-triage"], + "fields": [ + { + "id": "context", + "heading": "Context", + "kind": "textarea", + "required": true, + "description": "What surfaced this, and what leaving it costs." + }, + { + "id": "proposed-work", + "heading": "Proposed work", + "kind": "textarea", + "required": true, + "description": "The concrete next actions.", + "placeholder": "- ..." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "One verifiable check per line.", + "placeholder": "- [ ] ..." + }, + { + "id": "references", + "heading": "References", + "kind": "textarea", + "required": false, + "description": "Rules, files, pull requests, or docs this relates to." + } + ] + }, + "agent-ready": { + "form": "agent-ready.yml", + "name": "Agent-Ready Work Item", + "description": "Work written as an agent brief. Triage decides its type and whether it is agent-ready.", + "nativeType": null, + "labels": ["needs-triage"], + "fields": [ + { + "id": "summary", + "heading": "Summary", + "kind": "input", + "required": true, + "description": "The change, in one line." + }, + { + "id": "root-cause", + "heading": "Root cause", + "kind": "input", + "required": false, + "description": "Bugs only: unknown, or known: the cause in one line." + }, + { + "id": "current-behavior", + "heading": "Current behavior", + "kind": "textarea", + "required": true, + "description": "What the code does today." + }, + { + "id": "desired-behavior", + "heading": "Desired behavior", + "kind": "textarea", + "required": true, + "description": "What is true once this lands, edge cases and failure handling included. The end state, not edit instructions." + }, + { + "id": "key-interfaces", + "heading": "Key interfaces", + "kind": "textarea", + "required": false, + "description": "The types, signatures, config keys, or observable behaviors that change, named by contract rather than file path or line." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "Checks that each give a yes or no.", + "placeholder": "- [ ] ..." + }, + { + "id": "out-of-scope", + "heading": "Out of scope", + "kind": "textarea", + "required": true, + "description": "The neighboring code and features this change leaves as they are." + } + ] + }, + "decomposed-item": { + "form": null, + "name": "Decomposed work item", + "description": "A vertical slice /work-items:decompose publishes, born triaged; no public form.", + "nativeType": null, + "labels": [], + "fields": [ + { + "id": "outcome", + "heading": "Outcome", + "kind": "textarea", + "required": true, + "description": "The one observable change a user gets when this slice merges, end to end." + }, + { + "id": "key-interfaces", + "heading": "Key interfaces", + "kind": "textarea", + "required": true, + "description": "The design this slice touches, named by contract, or None." + }, + { + "id": "done-when", + "heading": "Done when", + "kind": "textarea", + "required": true, + "description": "The slice's acceptance criteria, each a check with a pass or fail answer." + }, + { + "id": "out-of-scope", + "heading": "Out of scope", + "kind": "textarea", + "required": true, + "description": "Adjacent work this slice must not change." + } + ] + } + } +} diff --git a/plugins/work-items/reference/issue-body-contract.json b/plugins/work-items/reference/issue-body-contract.json new file mode 100644 index 00000000000..93a7c625dea --- /dev/null +++ b/plugins/work-items/reference/issue-body-contract.json @@ -0,0 +1,235 @@ +{ + "schemaVersion": 1, + "headingLevels": [2, 3], + "emptyAnswer": "_No response_", + "floorLabels": ["needs-triage"], + "types": { + "bug": { + "form": "bug-report.yml", + "name": "Bug Report", + "description": "Report an unexpected problem or behavior.", + "nativeType": "Bug", + "labels": ["needs-triage"], + "fields": [ + { + "id": "steps", + "heading": "Steps to reproduce", + "kind": "textarea", + "required": true, + "description": "Minimal steps that reproduce the behavior.", + "placeholder": "1. ...\n2. ...\n3. ..." + }, + { + "id": "expected", + "heading": "Expected behavior", + "kind": "textarea", + "required": true, + "description": "What should happen?" + }, + { + "id": "actual", + "heading": "Actual behavior", + "kind": "textarea", + "required": true, + "description": "What actually happens? Include error messages, stack traces, or screenshots." + }, + { + "id": "severity", + "heading": "Severity", + "kind": "dropdown", + "required": true, + "description": "How bad is it? Say why under Severity justification.", + "options": ["low", "medium", "high", "critical"] + }, + { + "id": "severity-justification", + "heading": "Severity justification", + "kind": "textarea", + "required": true, + "description": "One sentence: the blast radius, what it blocks, or the data at risk." + }, + { + "id": "fix-location", + "heading": "Suggested fix location", + "kind": "input", + "required": false, + "description": "The file and function or class where the fix likely belongs, if known. No patch." + }, + { + "id": "environment", + "heading": "Environment", + "kind": "input", + "required": false, + "description": "OS, runtime and tool versions, and anything else relevant." + } + ] + }, + "feature": { + "form": "feature-request.yml", + "name": "Feature Request", + "description": "Propose a new feature or enhancement.", + "nativeType": "Feature", + "labels": ["needs-triage"], + "fields": [ + { + "id": "user-story", + "heading": "User story", + "kind": "textarea", + "required": true, + "description": "Who benefits and why?", + "placeholder": "As a [role], I want [capability] so that [benefit]." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "How do we know this is done? One verifiable check per line.", + "placeholder": "- [ ] Criterion 1\n- [ ] Criterion 2" + }, + { + "id": "area", + "heading": "Affected area / component", + "kind": "input", + "required": false, + "description": "Which part of the project would this affect?" + } + ] + }, + "task": { + "form": "task.yml", + "name": "Task", + "description": "A chore, maintenance task, audit, or other housekeeping item.", + "nativeType": "Task", + "labels": ["needs-triage"], + "fields": [ + { + "id": "context", + "heading": "Context", + "kind": "textarea", + "required": true, + "description": "What surfaced this, and what leaving it costs." + }, + { + "id": "proposed-work", + "heading": "Proposed work", + "kind": "textarea", + "required": true, + "description": "The concrete next actions.", + "placeholder": "- ..." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "One verifiable check per line.", + "placeholder": "- [ ] ..." + }, + { + "id": "references", + "heading": "References", + "kind": "textarea", + "required": false, + "description": "Rules, files, pull requests, or docs this relates to." + } + ] + }, + "agent-ready": { + "form": "agent-ready.yml", + "name": "Agent-Ready Work Item", + "description": "Work written as an agent brief. Triage decides its type and whether it is agent-ready.", + "nativeType": null, + "labels": ["needs-triage"], + "fields": [ + { + "id": "summary", + "heading": "Summary", + "kind": "input", + "required": true, + "description": "The change, in one line." + }, + { + "id": "root-cause", + "heading": "Root cause", + "kind": "input", + "required": false, + "description": "Bugs only: unknown, or known: the cause in one line." + }, + { + "id": "current-behavior", + "heading": "Current behavior", + "kind": "textarea", + "required": true, + "description": "What the code does today." + }, + { + "id": "desired-behavior", + "heading": "Desired behavior", + "kind": "textarea", + "required": true, + "description": "What is true once this lands, edge cases and failure handling included. The end state, not edit instructions." + }, + { + "id": "key-interfaces", + "heading": "Key interfaces", + "kind": "textarea", + "required": false, + "description": "The types, signatures, config keys, or observable behaviors that change, named by contract rather than file path or line." + }, + { + "id": "acceptance-criteria", + "heading": "Acceptance criteria", + "kind": "textarea", + "required": true, + "description": "Checks that each give a yes or no.", + "placeholder": "- [ ] ..." + }, + { + "id": "out-of-scope", + "heading": "Out of scope", + "kind": "textarea", + "required": true, + "description": "The neighboring code and features this change leaves as they are." + } + ] + }, + "decomposed-item": { + "form": null, + "name": "Decomposed work item", + "description": "A vertical slice /work-items:decompose publishes, born triaged; no public form.", + "nativeType": null, + "labels": [], + "fields": [ + { + "id": "outcome", + "heading": "Outcome", + "kind": "textarea", + "required": true, + "description": "The one observable change a user gets when this slice merges, end to end." + }, + { + "id": "key-interfaces", + "heading": "Key interfaces", + "kind": "textarea", + "required": true, + "description": "The design this slice touches, named by contract, or None." + }, + { + "id": "done-when", + "heading": "Done when", + "kind": "textarea", + "required": true, + "description": "The slice's acceptance criteria, each a check with a pass or fail answer." + }, + { + "id": "out-of-scope", + "heading": "Out of scope", + "kind": "textarea", + "required": true, + "description": "Adjacent work this slice must not change." + } + ] + } + } +} diff --git a/scripts/cross-plugin-source-registry.txt b/scripts/cross-plugin-source-registry.txt index d6c1587eb09..28d19d26d46 100644 --- a/scripts/cross-plugin-source-registry.txt +++ b/scripts/cross-plugin-source-registry.txt @@ -712,3 +712,11 @@ evals/fixtures/scaffold-guard.sh # repository but the run's own temporary home with the same guard. Canonical copy: # plugins/bugs/evals/fixtures/repo-scaffold-guard.sh. evals/fixtures/repo-scaffold-guard.sh + +# Dedicated check: scripts/sync-shared-copies.sh --check. Canonical: +# docs/contracts/issue-body/contract.json, synced from melodic-software/standards. +reference/issue-body-contract.json + +# Dedicated check: scripts/sync-shared-copies.sh --check. Cluster line for the +# duplication audit: the synced canonical contract and the copies. +docs/contracts/issue-body/contract.json -> plugins/*/reference/issue-body-contract.json diff --git a/scripts/shared-copies.txt b/scripts/shared-copies.txt index b7d82bca796..9f0a209a69d 100644 --- a/scripts/shared-copies.txt +++ b/scripts/shared-copies.txt @@ -804,3 +804,5 @@ docs/contracts/plugin-reconfiguration/README.md plugins/user-interface/reference .github/scripts/lib/lane-binding.mjs plugins/work-items/lib/lane-binding.mjs .github/scripts/lib/lane-binding.fixtures.mjs plugins/review/lib/lane-binding.fixtures.mjs .github/scripts/lib/lane-binding.fixtures.mjs plugins/work-items/lib/lane-binding.fixtures.mjs +docs/contracts/issue-body/contract.json plugins/bugs/reference/issue-body-contract.json +docs/contracts/issue-body/contract.json plugins/work-items/reference/issue-body-contract.json diff --git a/scripts/sync-shared-copies.sh b/scripts/sync-shared-copies.sh index 85df92e6d05..f9bb15b5fe8 100755 --- a/scripts/sync-shared-copies.sh +++ b/scripts/sync-shared-copies.sh @@ -101,10 +101,16 @@ load_registry() { # JavaScript copy takes a two-line comment header after any shebang; a Markdown # copy takes the same header as an HTML comment after any frontmatter block; an # HTML copy takes it as an HTML comment after a leading doctype line, which a -# browser needs first to stay out of quirks mode. +# browser needs first to stay out of quirks mode. JSON has no comment syntax, so +# a JSON copy is the canonical's bytes unchanged; shared-copies.txt and --check +# are what mark it generated. render() { local src="$1" prefix first="" close="" case "$src" in + *.json) + cat "$src" + return 0 + ;; *.mjs | *.cjs | *.js | *.ts) prefix='//' ;; *.sh | *.bash | *.py | *.ps1 | *.psm1 | *.awk) prefix='#' ;; *.html) diff --git a/scripts/sync-shared-copies.test.sh b/scripts/sync-shared-copies.test.sh index d7336087c99..bee2d8f6d2c 100755 --- a/scripts/sync-shared-copies.test.sh +++ b/scripts/sync-shared-copies.test.sh @@ -287,6 +287,22 @@ fi out="$(run --check)" expect "--check passes on generated Markdown copies" 0 $? "$out" +# --- a JSON copy is the canonical's bytes, with no header ------------------------- +fixture +printf '{\n "a": 1\n}\n' >"$root/lib/data.json" +printf 'lib/data.json plugins/alpha/ref/data.json\n' >"$root/scripts/shared-copies.txt" +run >/dev/null +if cmp -s "$root/lib/data.json" "$root/plugins/alpha/ref/data.json"; then + pass "a JSON copy is byte-identical to its canonical" +else + bad "a JSON copy is byte-identical to its canonical" "got: $(cat "$root/plugins/alpha/ref/data.json")" +fi +out="$(run --check)" +expect "--check passes on a generated JSON copy" 0 $? "$out" +printf '{\n "a": 2\n}\n' >"$root/plugins/alpha/ref/data.json" +out="$(run --check)" +expect "--check fails on an edited JSON copy" 1 $? "$out" + # --- an HTML copy takes an HTML-comment header, after any doctype --------------- fixture printf '<!doctype html>\n<html lang="en">\n</html>\n' >"$root/lib/page.html" @@ -342,7 +358,7 @@ reject "a copy registered twice is rejected" "registered twice" "lib/esc.mjs plu reject "a canonical registered as a copy is rejected" "both a canonical and a copy" \ "plugins/alpha/lib/esc.mjs plugins/gamma/lib/esc.mjs" plugins/alpha/lib/esc.mjs reject "an extension with no known comment syntax is rejected" "no comment syntax known" \ - "lib/data.json plugins/alpha/data.json" lib/data.json + "lib/data.toml plugins/alpha/data.toml" lib/data.toml fixture out="$(run --bogus)" From 89c3df1b22a69da612e323cf79fbb45cd3512d0d Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:58:24 -0500 Subject: [PATCH 5/7] fix(work-items): count a ### sub-section's text toward its ## field check-body.mjs replaced the open ## section when a ### heading appeared, so a field answered under a sub-heading read as empty. Keep a stack of open sections and give each prose line to every section still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- .../skills/triage/scripts/check-body.mjs | 16 +++++++++------- .../skills/triage/scripts/check-body.test.mjs | 10 ++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/plugins/work-items/skills/triage/scripts/check-body.mjs b/plugins/work-items/skills/triage/scripts/check-body.mjs index 45549d4d7ec..1c99d55b3f5 100755 --- a/plugins/work-items/skills/triage/scripts/check-body.mjs +++ b/plugins/work-items/skills/triage/scripts/check-body.mjs @@ -4,7 +4,8 @@ // (plugins/work-items/reference/issue-body-contract.json, synced from melodic-software/standards). // A field counts as present under a heading at any of the contract's `headingLevels` (`##` from a // skill, `###` from an issue form), with text other than the contract's `emptyAnswer` before the -// next heading at that level or shallower. HTML comments and fenced code do not count. +// next heading at that level or shallower, deeper sub-sections included. HTML comments and fenced +// code do not count. // // Usage: check-body.mjs --type <type> --body-file <path> [--contract <path>] // Prints one JSON object: {type, missing: [heading...], empty: [heading...], unknownType}. @@ -39,23 +40,24 @@ function proseLines(body) { }); } -// Maps each normalized heading at an allowed level to the text under it. +// Maps each normalized heading at an allowed level to the text under it, its sub-sections' text included. export function sections(body, levels) { const found = new Map(); - let open = null; + const open = []; for (const line of proseLines(body)) { const heading = line.match(/^ {0,3}(#{1,6})[ \t]+(.*?)[ \t#]*$/); if (heading) { const depth = heading[1].length; - if (open && depth <= open.depth) open = null; + while (open.length && open.at(-1).depth >= depth) open.pop(); if (levels.includes(depth)) { - open = { depth, text: [] }; + const section = { depth, text: [] }; + open.push(section); const key = normalize(heading[2]); - if (!found.has(key)) found.set(key, open.text); + if (!found.has(key)) found.set(key, section.text); } continue; } - open?.text.push(line); + for (const section of open) section.text.push(line); } return found; } diff --git a/plugins/work-items/skills/triage/scripts/check-body.test.mjs b/plugins/work-items/skills/triage/scripts/check-body.test.mjs index 870d0a02cc9..c1efa68e9cf 100644 --- a/plugins/work-items/skills/triage/scripts/check-body.test.mjs +++ b/plugins/work-items/skills/triage/scripts/check-body.test.mjs @@ -58,6 +58,16 @@ describe("checkBody", () => { assert.deepEqual(checkBody(contract, "task", body).empty, []); }); + it("counts an allowed-level sub-heading's text toward its parent field", () => { + const body = "## Context\n\n### Background\n\nWhy.\n\n## Proposed work\n\n- x\n"; + assert.deepEqual(checkBody(contract, "task", body).empty, []); + }); + + it("closes a sub-section at the next parent-level heading", () => { + const body = "## Context\n\n### Background\n\nWhy.\n\n## Proposed work\n\n_No response_\n"; + assert.deepEqual(checkBody(contract, "task", body).empty, ["Proposed work"]); + }); + it("flags an unknown type", () => { assert.equal(checkBody(contract, "epic", "").unknownType, true); }); From cb3a4598af7c45b68a6677e49b747ee4440f0b04 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 19:04:23 -0500 Subject: [PATCH 6/7] test(work-items): add a discovery wrapper for check-body.test.mjs plan-test-lanes.sh failed select-tests because check-body.test.mjs had no sibling .test.sh and no Node package in the test-node lane holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- .../skills/triage/scripts/check-body.test.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 plugins/work-items/skills/triage/scripts/check-body.test.sh diff --git a/plugins/work-items/skills/triage/scripts/check-body.test.sh b/plugins/work-items/skills/triage/scripts/check-body.test.sh new file mode 100755 index 00000000000..2286c4bd3b6 --- /dev/null +++ b/plugins/work-items/skills/triage/scripts/check-body.test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Discovery wrapper for check-body.test.mjs: run-plugin-tests.sh discovers only *.test.sh, +# so the cases live in the .mjs file and this only runs node and forwards its exit status. +set -uo pipefail + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if ! command -v node >/dev/null 2>&1; then + echo "SKIP: node not installed" >&2 + exit 0 +fi + +node --test "$SELF_DIR/check-body.test.mjs" From 1afd90696ff567d2d313e01893532900afa8ab64 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 19:18:58 -0500 Subject: [PATCH 7/7] fix(work-items): name the issue-body contract for the vocabulary check check-vocabulary read "contract" near "brief" as the retired planning Brief sense. Allow the named issue-body contract, and name it on the two lines that said only "contract". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe --- plugins/work-items/reference/agent-brief.md | 2 +- plugins/work-items/skills/track/actions/add.md | 2 +- scripts/test_check_vocabulary.py | 5 +++++ scripts/vocabulary-senses.json | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/work-items/reference/agent-brief.md b/plugins/work-items/reference/agent-brief.md index 8577358f2a6..05317d0956b 100644 --- a/plugins/work-items/reference/agent-brief.md +++ b/plugins/work-items/reference/agent-brief.md @@ -31,7 +31,7 @@ Each pair below comes from one of two changes, a retry backoff cap and a CSV exp The brief's fields are the `agent-ready` type of the issue-body contract ([`${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json`](${CLAUDE_PLUGIN_ROOT}/reference/issue-body-contract.json)), the record the org agent-ready issue form is generated from. Write one `### <heading>` per entry of -its `fields`, in contract order, under the `## Agent Brief` heading; a form-filed item already +its `fields`, in issue-body contract order, under the `## Agent Brief` heading; a form-filed item already carries them as `###` sections. The type is the issue's own (native Issue Type on org repos, `type:` label on personal / non-org repos), never a brief field. Each field's `description` says what it holds; the rules table above says how to write the four that go stale. Shape: diff --git a/plugins/work-items/skills/track/actions/add.md b/plugins/work-items/skills/track/actions/add.md index 3032a0a4a12..86674d4c3ae 100644 --- a/plugins/work-items/skills/track/actions/add.md +++ b/plugins/work-items/skills/track/actions/add.md @@ -52,7 +52,7 @@ Create a new work item with labels from the taxonomy. 1. **Build labels list** `{labels}` (comma-separated for the seam) from the remaining flags. With no `--priority` and no `--agent-ready`, include no `priority:` label and include the bare `needs-triage` floor when the live set has it. A supplied `--priority`, or `--agent-ready`, does not leave the floor on: a known tier or a complete brief is already past intake. Also start from `category:general` and replace it with a supplied `--category`. Append `--area`/`--ecosystem` labels when provided. When `--agent-ready` is set, also append the autonomous-eligible role label (default `agent-ready`) so the item is eligible for autonomous pickup. A default that the consuming repo doesn't define is omitted rather than passed. With `--parent`, or a `--label` that is the binding's container label (`config.container_label`, default `work-map`), skip the `category:general` default and the `needs-triage` floor: a container or its child is not intake. Append each `--label` verbatim, after checking them as the flag says. -1. **Build body.** With `--body-file`, the file is the body. With `--parent` or the container label and no `--body-file`, the description is the body. Otherwise, if `--agent-ready`, use the agent-brief template from [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md), whose fields are the contract's `agent-ready` type. Otherwise take the contract type for the resolved issue type (`Bug` → `bug`, `Feature` → `feature`, `Task` → `task`) and write one `## <heading>` section per entry of its `fields`, in contract order, each filled from the description and `--context` as its `description` asks, with every required field filled (a field nothing backs reads `unknown`, never invented). Then append the sections below, which the contract does not hold: +1. **Build body.** With `--body-file`, the file is the body. With `--parent` or the container label and no `--body-file`, the description is the body. Otherwise, if `--agent-ready`, use the agent-brief template from [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md), whose fields are the issue-body contract's `agent-ready` type. Otherwise take the contract type for the resolved issue type (`Bug` → `bug`, `Feature` → `feature`, `Task` → `task`) and write one `## <heading>` section per entry of its `fields`, in contract order, each filled from the description and `--context` as its `description` asks, with every required field filled (a field nothing backs reads `unknown`, never invented). Then append the sections below, which the contract does not hold: ```markdown ## Metadata diff --git a/scripts/test_check_vocabulary.py b/scripts/test_check_vocabulary.py index 29be6fa84f3..cfe3a45b580 100755 --- a/scripts/test_check_vocabulary.py +++ b/scripts/test_check_vocabulary.py @@ -717,6 +717,11 @@ def eval_rows(sense: str, line: str): r"\bname[- ]by[- ]contract\b", "The brief follows the name-by-contract rule.", ), + phrase( + "contract-brief", + r"\bissue-body contract\b", + "The brief's fields are the agent-ready type of the issue-body contract.", + ), phrase( "contract-brief", r"\bcontract case\b", diff --git a/scripts/vocabulary-senses.json b/scripts/vocabulary-senses.json index 48c8ae14681..31d956d15bf 100644 --- a/scripts/vocabulary-senses.json +++ b/scripts/vocabulary-senses.json @@ -196,6 +196,7 @@ {"phrase": "\\binterface contracts?\\b", "reason": "an interface contract at design"}, {"phrase": "\\b(pr|api|feed|payload|behavioral|presentation|execution|detection|return|framing|findings)[- ]contracts?\\b", "reason": "a named contract that is not the Brief: a PR, API, feed or payload contract, the agent brief's behavioral contract, plan's presentation contract, the interview's execution contract, the handoff detection contract, an agent's return contract, a spawn brief's framing contract or a review's findings contract"}, {"phrase": "\\bname[- ]by[- ]contract\\b", "reason": "the agent brief's name-by-contract rule"}, + {"phrase": "\\bissue-body contract\\b", "reason": "the org issue-body contract synced from melodic-software/standards, whose agent-ready type holds the Agent Brief fields"}, {"phrase": "\\bcontract case\\b", "reason": "an eval's Contract case label names a behavior case, not the Brief"}, {"path": "**/evals/**/samples/**", "reason": "recorded model output; an edit changes what the eval measures"}, {"path": "**/evals/**/graders/**", "reason": "grader patterns matched against recorded samples; an edit changes what the eval measures"},