agentleFS
kenmulfordkenmulford/milestone-driver/skills/triage/SKILL.md

triage

Skill2 starsChanged 16 days agoMITView on GitHub

This skill should be used when the user invokes "/milestone-driver:triage <milestone-name | issue-number>", or asks to "triage the milestone/issue", "triage the milestone", "review for gaps", or "review this issue for gaps". Reviews issues for design gaps and dependency ordering before building - the Layer 0 pre-build phase. Authors no code; opens no PRs.

189 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use writing-beats, setup-matt-pocock-skills and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • LesaniSkill

    writing-beats

    Shape an article as a journey of beats, choose-your-own-adventure style. The user picks a starting beat from the raw material, you write only that beat, then offer options for where to pivot next, beat by beat, until the article reaches a natural end. Use when the user has raw material and wants to assemble it as a narrative rather than an argument.

    Also in 66 projects that use this

  • GoogleSkill

    setup-matt-pocock-skills

    Configure this repo for the engineering skills — set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.

    Also in 167 projects that use this

  • liufeng1976Skill

    setup-ts-deep-modules

    Wire dependency-cruiser into a TypeScript repo so each package is a deep module — implementation hidden in subfolders, reachable only through its entry-point files. User-invoked.

    Also in 42 projects that use this

  • SethGammonSkill

    ascii-diagram

    Generate perfectly aligned ASCII diagrams — architecture, flow, sequence, box-and-arrow. Uses a programmatic character-grid approach so alignment is guaranteed by math, not token prediction. Includes post-render verification.

    Also in 4 projects that use this

  • simo97Skill

    migrate-to-shoehorn

    Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.

    Also in 68 projects that use this

  • LesaniSkill

    writing-shape

    Take a markdown file of raw material and shape it into an article through a conversational session — drafting candidate openings, growing the piece paragraph by paragraph, arguing about format (lists, tables, callouts, quotes) at each step. Use when the user has a pile of notes, fragments, or a rough draft and wants help turning it into something publishable.

    Also in 63 projects that use this

  • +1 more from Lesani
  • Other versions189 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 189

    1. mattpocock/skills★ 309ksame name
    2. withastro/astro★ 64ksame name
    3. vectorize-io/hindsight★ 60ksame name
    4. ogulcancelik/herdr★ 47ksame name
    5. herdrdev/herdr★ 47ksame name
    6. colinhacks/zod★ 44ksame name
    7. QuivrHQ/quivr★ 40ksame name
    8. The-Vibe-Company/quivr★ 40ksame name
    9. vercel-labs/add-skill★ 39ksame name
    10. vercel-labs/skills★ 39ksame name

    179 more not listed

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository3 other files it gives its agents.

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 3 are distinctive, and 8 are very specific to this project.

  • 0

    Common

    Most of the 176 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If ${CLAUDE_PLUGIN_ROOT} appears unexpanded, it is the parent of the skills/ directory that holds this file.

    line 5
  2. Very specific:

    Read the milestone description, redirected to <scratch>/milestone.md:

    line 53
  3. Very specific:

    Source the docs root: Use projectDocs already resolved at Step 1 (defaults to .project/).

    line 105
  4. Very specific:

    After aggregating gaps for each issue, classify it as light or heavy (default heavy when inconclusive).

    line 206
  5. Very specific:

    Classify as light: only when all of the following hold:

    line 219
  6. Distinctive:

    All triage criteria are clean (no Blockers from either lens).

    line 221
  7. Very specific:

    Not both NEEDS_DESIGN_REVIEW: yes and a UI surface.

    line 224
  8. Distinctive:

    When the Step 6.5 cache write was skipped or failed this run (jq absent on the Bash path, or any write error on either path), append one …

    line 243
  9. Distinctive:

    After posting Step 6's comments, write/update entries for every freshly-triaged (MISS) issue.

    line 329
  10. Very specific:

    clearLabel defaults to false when a return omits it: - only an explicit true removes a label.

    line 366
  11. Very specific:

    Read ${CLAUDE_PLUGIN_ROOT}/skills/citation-format.md - the one format every citation in those slots takes.

    line 381

“Common” is measured: kept by at least 6 in 10 of the 176 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

If ${CLAUDE_PLUGIN_ROOT} appears unexpanded, it is the parent of the skills/ directory that holds this file.

triage - pre-build review phase

Review issues for design gaps and dependency ordering. Emit an all-clear or a gap table. Post a blocker summary on each affected issue. Return the validated dependency graph.

Contents

1 Profile · 2 Gather · 2.5 Cache lookup · Resolve docs · Resolve citations · 3 Dispatch · 3.5 Resolve · 4 Aggregate · 5 Output · 6 Comment · 6.5 Cache write · 7 Return

Announce first

Say this to the user before doing any work:

Standing by while I review the issue(s) for gaps and dependencies needing your input before building.

Modes

ArgumentModeScope
Milestone name (string)BatchAll open issues in that milestone
Issue number (integer)SingleThat one issue

Procedure

Step 1 - Read the profile

Read the profile (docs/profile-schema.md). Resolution (read only - triage performs no migration move): <repo>/.milestone-config/driver.json first, else the legacy root <repo>/milestone-driver.json. Both present → .milestone-config/driver.json wins; no move, no overwrite, no deletion. On the legacy layout, triage surfaces a one-line note - "legacy profile detected - will migrate on the next build/setup". If neither file exists or a required Core key is missing, invoke milestone-driver:setup, then continue.

Extract:

KeyDefault
triageAgentmilestone-driver:triage-reviewer
designReviewAgentmilestone-driver:design-reviewer
blockerResolverAgentmilestone-driver:blocker-resolver
uiSurfaceGlobs(absent → no design-lens review)
sourceGlobs(pass through to the agent brief in Step 3)
nonNegotiables(pass through to the agent brief in Step 3)
domainSkillsExpand before use: ${CLAUDE_PLUGIN_ROOT}/scripts/expand-domain-skills.{sh,ps1} ~/.claude/plugins/cache <entry>… (pwsh on Windows, bash elsewhere). Its stdout is what the Step 3 briefs carry, never the raw profile value; each unresolved: <entry> on stderr is named there as domainSkills unresolved: <entry>. Empty stdout is an absent key: the briefs omit domainSkills and still name those entries.
projectDocs.project/

Step 2 - Gather issues

<scratch> is .milestone-config/.runtime/triage/ (gitignored by .milestone-config/.gitignore). Create it first: mkdir -p <scratch> (pwsh: New-Item -ItemType Directory -Force <scratch>). Every full-text fetch below redirects to a file there - none reaches the terminal or this skill's output.

Batch mode (argument is a milestone name):

  1. Read the milestone description, redirected to <scratch>/milestone.md:

    gh api "repos/{owner}/{repo}/milestones" \
      --jq '.[] | select(.title=="<milestone-name>") | .description' > <scratch>/milestone.md
  2. List all open issues (number/title/labels only, for ordering and Step 2.5's partition):

    gh issue list --milestone "<milestone-name>" --state open --json number,title,labels
  3. For EACH issue number, redirect its full record - title, body, labels, comments - to <scratch>/issue-<n>.md, with the ## Body and ## Comments headings the later steps read:

    gh issue view <n> --json number,title,body,labels,comments --template '# #{{.number}} {{.title}}{{"\n\n"}}Labels: {{range .labels}}{{.name}} {{end}}{{"\n\n"}}## Body{{"\n\n"}}{{.body}}{{"\n\n"}}## Comments{{"\n"}}{{range .comments}}{{"\n"}}### {{.author.login}} {{.createdAt}}{{"\n\n"}}{{.body}}{{"\n"}}{{end}}' > <scratch>/issue-<n>.md

    Runs unchanged under pwsh - single quotes are literal in both shells.

Single mode (argument is an issue number) - batch-mode item 3's gh issue view line, for the one issue.

Both modes end with the same Step 3 inputs: each issue's number and its full-record path <scratch>/issue-<n>.md.

Step 2.5 - Cache lookup (before dispatching agents)

The cache mechanics - path resolution, the change-signal key, and the batched GraphQL text - live in ${CLAUDE_PLUGIN_ROOT}/scripts/triage-cache.{sh,ps1} (pwsh on Windows, bash elsewhere - same host selection as the two resolvers below). Do not re-derive them here. The script never runs gh: it prints the query for you to run and parses the response you hand back. It never errors the run - an absent, unreadable, or invalid cache file is an empty cache, and every degraded path exits 0.

  1. Fetch the change signals. triage-cache.<sh|ps1> query keys <owner> <repo> <n>… prints one batched, aliased GraphQL query covering every issue gathered in Step 2. Run it (gh api graphql -f query="$(…)") and save the response - keep that file for Step 6.5, which recomputes the cache keys from it. Fall back to per-issue gh issue view <n> --json … calls only if the batch call fails - collect those results into the same issue_<n> alias shape the batch returns ({"issue_<n>": {"issue": {"createdAt": …, "comments": {"totalCount": …}, "labels": {"nodes": [{"name": …}]}}}, …}) and save that assembled file: lookup here and write at Step 6.5 must read the same one. If even the fallback produces no file, still pass its path at Step 6.5: write treats an absent response as fail-open (entries stored with no key, exit 0) and those issues re-triage next run. bodyLastEditedAt is not a valid field for gh issue view --json - use lastEditedAt via GraphQL only, so an assembled fallback file omits it and createdAt supplies the timestamp on both legs.
  2. Partition. triage-cache.<sh|ps1> lookup <repo-root> <response.json> prints TAB-separated records: HIT<TAB><n>; MISS<TAB><n><TAB><reason> (no-entry, key-mismatch, no-live-key); EDGES<TAB><n>…, the deduplicated union of every HIT candidate's cached result.edges; and SUMMARY<TAB>hits=<h><TAB>misses=<m>. A single SKIP<TAB><reason> record replaces the whole set when no cache information is available (no-jq, bad-response) - treat every issue as a MISS.
  3. Check the edges. When the EDGES record carries at least one number, run triage-cache.<sh|ps1> query edges <owner> <repo> <those numbers> through gh api graphql, then triage-cache.<sh|ps1> check-edges <repo-root> <response.json>. Fall back to per-issue gh issue view <n> --json state,stateReason only when the batch query fails - collect those results into the same issue_<n> alias shape ({"issue_<n>": {"issue": {"state": …, "stateReason": …}}, …}) and hand that file to check-edges. Each MISS<TAB><n><TAB>stale-edge it prints downgrades that HIT to a MISS; SUMMARY<TAB>stale=<k> closes the set. Its own SKIP<TAB>bad-response - reached only once the per-issue fallback has also failed - means the downgrade could not be computed; downgrade every HIT.

The partition contract those records implement:

ResultConditionAction
HITCached entry exists and key matches the live key and no stale-edge condition (see below)Reuse cached result; do not dispatch triageAgent; do not dispatch designReviewAgent
MISSNo entry OR key mismatch OR stale-edge conditionProceed to Step 3 dispatch normally

Stale-edge states are run-scoped: check-edges reads them from the response file you pass it, and nothing about them is written to the cache.

Stale-edge invalidation rule (a HIT can still be downgraded): check-edges forces re-triage on state == "CLOSED" with stateReason != "COMPLETED" - a dependency closed without merging.

Partition issues into HIT set (cache-reused) and MISS set (fresh dispatch needed). Carry both sets forward.

Single mode: cache lookup, key comparison, and the stale-edge invalidation check all apply identically to the one issue.

Resolve cited project-docs sections (once per issue, before dispatch)

Resolve each issue's cited .project/ sections once, here in the triage skill, for every issue in the MISS set (HIT issues skip dispatch). It is additive grounding: it only adds an input to the two dispatch briefs (Step 3).

  1. Source the docs root. Use projectDocs already resolved at Step 1 (defaults to .project/). Do not re-resolve the profile here.
  2. Parse the cited anchors. Collect the .project/<doc>#<section> anchors (<doc> the docs-root path, <section> the heading text, e.g. design-system.md#data-tables) from each MISS issue's record file (<scratch>/issue-<n>.md) via a targeted extraction, never a whole-file read (same discipline as skills/solve-milestone/SKILL.md § Main-thread context):

    grep -o '\.project/[A-Za-z0-9_./-]*#[^ )`]*' <scratch>/issue-<n>.md | sort -u
    # pwsh: (Select-String -Path <scratch>/issue-<n>.md -Pattern '\.project/[A-Za-z0-9_./-]+#[^ )`]+' -AllMatches).Matches.Value | Sort-Object -Unique
  3. Pass the anchors, not the sections. Put each MISS issue's cited anchors - plus plausibly-relevant sibling section names - into both the triageAgent and designReviewAgent briefs composed in Step 3 as the cited .project/ anchors, with the absolute path of ${CLAUDE_PLUGIN_ROOT}/scripts/read-doc-section.{sh,ps1}. Each reviewer reads them with that primitive (read-doc-section.<sh|ps1> <doc-path> <anchor-text>, <doc-path> the doc under the docs root, <anchor-text> the heading text without leading #s; it prints only that section) and keeps its own Read/grep tools for any additional anchor. Bias toward over-inclusion in the list. Never inline a section, never a whole file.
  4. Name the prose contract (once per run). Put the absolute path of ${CLAUDE_PLUGIN_ROOT}/skills/output-style.md into both Step 3 briefs as the prose contract path, naming the sections each reviewer reads: ## GitHub-facing prose, ## When prose is the correct form, ## Evidence slots, ## The two anti-criteria. Never paste them. They govern each reviewer's returned description and to_clear lines - the text this skill renders verbatim into the 🔴 Triage comment at Step 6 - and each agent's own ## Communication style may specialize them but never replace them.

Degradation (no error, ever):

  • Absent projectDocs → defaults to .project/ (resolved at Step 1).
  • Absent .project/ directory (or no cited anchors on an issue) → a no-op for that issue: dispatch proceeds with no project grounding and no error.
  • Missing/renamed cited anchor → the primitive fails loud (non-zero exit, naming the anchor + file on stderr) so a drifted heading surfaces instead of silent empty grounding. Do not swallow it.
  • Absent or unreadable skills/output-style.md → a no-op: the briefs name no path, no error, each reviewer's own ## Communication style its only prose rule.

Resolve cited path (anchor) citations (once per issue, before dispatch)

Resolve each MISS issue's path (anchor) citations (skills/citation-format.md) once here - a HIT issue skips dispatch and makes no resolver call. Paths are repo-root-relative; no multi-base fallback.

  1. The skill's one bounded read of an issue body. Per MISS issue, read <scratch>/issue-<n>.md's ## Body section with sed -n '/^## Body$/,/^## Comments$/p' <scratch>/issue-<n>.md (never read-doc-section: it stops at the body's own first ##) - same discipline as skills/solve-milestone/SKILL.md § Main-thread context. Then extract by model judgment over the path (anchor) shape - never a regex: apply its span and position tests to that section - a parenthetical after a path is not automatically a citation. Both regex failure modes, on prose whose span closes before the parenthesis: `agents/triage-reviewer.md` (architect lens) exits 1 - a false drift report; `skills/setup/SKILL.md` (Phase 2) returns a PRIMARY row on a prose mention of "Phase 2" above the heading - a confident wrong answer.
  2. Resolve, then feed both briefs. Invoke ${CLAUDE_PLUGIN_ROOT}/scripts/resolve-citation.{sh,ps1} (pwsh on Windows, bash elsewhere) once per citation, with <file-path> <anchor-text> as arguments. Exit 0 prints a PRIMARY row then zero or more MATCH rows, TAB-delimited, file order. Pass those rows, as printed, into both Step 3 briefs as the resolved citations - once per issue, not once per reviewer.

Degradation:

  • No path (anchor) citation on an issue → a no-op: no resolver call, no error.
  • A cited anchor not found, or an unreadable file → resolve-citation fails loud (nonzero; anchor + file on stderr, stdout empty). Surface it; never swallow it.

Step 3 - Dispatch triageAgent per issue

Dispatch the agent named in triageAgent (default milestone-driver:triage-reviewer) for each issue in the MISS set only. Dispatches are parallelizable: run them concurrently where the tool environment supports it. Briefs carry resolved absolute paths, never the token (Codex: skills/runtime-codex.md). The brief must also carry this scratch-hygiene rule: write scratch only under a path named for that issue or that agent, never the shared scratchpad directory, and report what a probe printed rather than writing a probe file to read back later. The brief must also carry this command-shape rule: absolute paths only, and never change directory (cd, pushd, or a subshell); git -C <dir> for git, absolute file paths for grep, cat, sed, and find; a command needing a working directory (unitTestCmd, preflightCmd, npm ci) runs with the absolute worktree path as its cwd.

Brief each agent with:

  • The issue: the path of <scratch>/issue-<n>.md (title, body, acceptance criteria, labels) - read right after the skills, never pasted inline.
  • Its recorded design decisions: the same file (comments and any design-cleared notes are part of its record).
  • The milestone description: the path of <scratch>/milestone.md - batch mode only; pass an empty string in single mode.
  • The profile: sourceGlobs, uiSurfaceGlobs, nonNegotiables, domainSkills (the agent invokes each name first, before reading any file; omit when the expansion is empty).
  • The cited .project/ anchors for this issue, with the read-doc-section path - omit when that block was a no-op for this issue.
  • The prose contract path and its four section names; omit when skills/output-style.md is absent.
  • The resolved citations for this issue; omit when that block was a no-op.
  • citationFormatPath - the absolute path of ${CLAUDE_PLUGIN_ROOT}/skills/citation-format.md; always passed, never omitted.
  • The repo root - it bounds the agent's read scope; a re-triage running inside an issue worktree names that worktree instead.

Each agent returns:

ISSUE: <n>
DEPENDS_ON: [<issue numbers>]   # validated edges, with one-line reasons
NEEDS_DESIGN_REVIEW: yes | no
DOMAIN_SKILLS_INVOKED: <comma-separated exact names> | none
GAPS:
  - lens: architect
    severity: Blocker | Advisory
    type: contradiction | not-buildable | missing-criteria | undeclared-dependency | risky-design
    description: <one line>
    to_clear: <the one decision or artifact the human must record, as an instruction they can act on without reading the rest of the block, plus its evidence reference (per `citationFormatPath`) when one exists - structural, not a word count; two decisions here is two gaps (skills/output-style.md, "to_clear field" row)>
  - … (or "none")

For each MISS issue whose triageAgent return carries NEEDS_DESIGN_REVIEW: yes, dispatch designReviewAgent (default milestone-driver:design-reviewer). The brief must also carry the same scratch-hygiene and command-shape rules as above.

Brief the design agent with:

  • The issue: the path of <scratch>/issue-<n>.md (title, body, acceptance criteria) - read right after the skills, never pasted inline.
  • Its recorded design decisions: the same file.
  • Pointers to existing UI surfaces the issue neighbors - via uiSurfaceGlobs from the profile.
  • The profile: uiSurfaceGlobs, domainSkills (invoked first, as in the triageAgent brief above; omit when the expansion is empty).
  • The cited .project/ anchors, the prose contract path, and the resolved citations - the same ones passed to the triageAgent above; omit each when its block was a no-op. Plus citationFormatPath, never omitted.
  • The repo root - same as above.

The design agent returns:

ISSUE: <n>
DOMAIN_SKILLS_INVOKED: <comma-separated exact names> | none
GAPS:
  - lens: design
    severity: Blocker | Advisory
    type: spec-insufficiency | scalability | pattern-inconsistency | missing-state | missing-affordance | accessibility
    description: <one line>
    to_clear: <the one suggested resolution or reference pattern (e.g. "group under collection headers like ConfirmImportPage"), as an instruction the human can act on without reading the rest of the block, plus its evidence reference (per `citationFormatPath`) when one exists - structural, not a word count; two resolutions here is two gaps (skills/output-style.md, "to_clear field" row)>
  - … (or "none")

Domain-skills gate. A triageAgent, design-agent, or Step 3.5 resolver return whose DOMAIN_SKILLS_INVOKED misses an expanded domainSkills name is re-dispatched once, same brief plus the missing names; the new return replaces the first, its NEEDS_DESIGN_REVIEW and GAPS included. A second miss keeps that return; Step 5's output and any Step 6 comment on that issue carry Domain skills missed: <names>.

Step 3.5 - Resolve Blockers before they park

A MISS issue with ≥1 severity: Blocker gap → read ${CLAUDE_PLUGIN_ROOT}/skills/triage/blocker-resolver-dispatch.md and run it. Nothing else reaches it.

Step 4 - Aggregate findings

Merge cached and fresh results first. Combine the HIT set's cached result objects (Step 2.5) with the fresh agent returns (Step 3) into one result set covering all issues.

  • HIT issues: the cached result carries blockers, label, advisories, risk, and edges - use them directly. Risk comes from the cached risk value (the cache key's label component guarantees override labels have not changed).
  • MISS issues: use the fresh agent returns as normal.

Collect all GAPS across all results for each issue. Aggregate by lens / severity / description / to_clear - the type tokens differ between the two agents; match on the other fields, not type.

Risk classification

After aggregating gaps for each issue, classify it as light or heavy (default heavy when inconclusive). Store the result in issueStates[n].risk (returned at Step 7).

Operator override labels (checked first). A risk:heavy or risk:light label sets the profile directly - skip the rubric below. When both are present, risk:heavy wins.

Observable rubric (runs only when no override label is present, over the pre-resolution gap set).

Classify as heavy when any of the following is true:

  • A triage gap of type contradiction or not-buildable is present.
  • The triageAgent adds an undeclared DEPENDS_ON edge (one not declared in the milestone's Wave order).
  • NEEDS_DESIGN_REVIEW: yes and the issue names or touches a UI surface.
  • The issue body names a shared interface, schema, auth path, or payment path.
  • Classification is ambiguous (default heavy).

Classify as light only when all of the following hold:

  • None of the heavy conditions above is triggered.
  • All triage criteria are clean (no Blockers from either lens).
  • The issue body names no shared interface, schema, auth path, or payment path.
  • The triageAgent adds no undeclared DEPENDS_ON edges.
  • Not both NEEDS_DESIGN_REVIEW: yes and a UI surface.

Build the validated dependency graph from all DEPENDS_ON edges across the merged result set:

  • Preserve the per-issue edges exactly as returned by each triageAgent (before any wave aggregation) - plus the edges carried in each HIT issue's cached result - these together form the edges map in the returned dependencyGraph (Step 7).
  • Rebuild dependencyGraph.waves from the merged per-issue edges map plus the milestone's declared Wave order, using a pure in-context topological sort.
  • Where an agent finds an undeclared dependency, add it to the graph - it surfaces as an Advisory unless the reviewer returned it as a Blocker (ungrounded after the source set, or cyclic with the Wave order or the other edges, per agents/triage-reviewer.md § Severity rule).
  • Produce the Wave-ordered graph for output and maintain the raw per-issue edges map alongside it.

Step 5 - Output to the user

Open every output block with the cache split. Example:

Triage: 4 reused (cache), 2 fresh; 3 blockers resolved, 1 needs human

(Substitute the actual counts. All HIT: Triage: N reused (cache), 0 fresh. All MISS: Triage: 0 reused (cache), N fresh.) Append the resolve split only when Step 3.5 ran.

When the Step 6.5 cache write was skipped or failed this run (jq absent on the Bash path, or any write error on either path), append one clause to that same line:

Triage: 4 reused (cache), 2 fresh; cache write skipped this run

Show the clause only when a skip/failure occurred.

All clear (no Blocker gaps across all issues):

✅ All clear

Wave-ordered dependency graph:
  Wave 1 (parallel): #A, #B, #C
  Wave 2: #D (depends on #A, #B)
  Wave 3: #E (depends on #D)

Advisory notes:
  #B - <one-line advisory>

Omit "Advisory notes" when there are none.

Gaps present (any issue has one or more Blocker gaps) - emit a table, Blockers first:

IssueLensSeverityGapWhat's needed
#43architectBlockerRecorded design is internally contradictory: "mirror ConfirmImportPage grouping" vs "flat list, no collection picker"Record the authoritative grouping decision on the issue
#43designBlockerFlat 16-row list at realistic volume will produce a poor result vs established grouped-card patternGroup under collection headers as in ConfirmImportPage
#37architectAdvisoryNo empty-state criterion specifiedRecord expected empty-state behavior

Blockers sort before Advisories within each issue. Issues sort by number ascending.

Include the Wave-ordered dependency graph after the table, gaps or not.

Step 6 - Comment on each affected issue and recommend its park label

For every freshly-triaged (MISS) issue with surviving Blocker gaps or a Step 3.5 RESOLVED verdict:

Cache-hit Blocker issues do not receive a duplicate 🔴 Triage comment. Their original comment from the first run persists.

Previously-blockered issues that get a MISS always post a fresh comment. When a cached entry shows blockers: true but the cache is invalidated, the re-triage is a full MISS. If the result still has Blockers, post a fresh 🔴 Triage comment - do not guard on the stale cached blockers: true to skip posting.

Accepted trade-off: the 🔴 Triage comment posts after the cache key is computed, incrementing the issue's comment count and self-invalidating the entry so a blockered issue re-triages fresh. Do not add a dedup guard for it; the 🟢 Resolved comment has its own (skills/triage/blocker-resolver-dispatch.md (Unparking)).

For each qualifying MISS issue:

  1. Post a 🟢 Resolved comment on ≥1 Step 3.5 RESOLVED, in the shape at skills/output-style.md (Resolved comment).
  2. Post a triage comment (gh issue comment <n> --body "...") in the triage-comment shape (skills/output-style.md) on ≥1 surviving Blocker. The comment body must:

    • Open with 🔴 Triage - byte-fixed, parsed downstream at skills/solve-milestone/SKILL.md (Issues parked) and probed at skills/solve-milestone/parallel-waves.md (the probe found a park label). Only what FOLLOWS the opener is structured here.
    • Render the surviving Blocker gaps as a structured table, one row per gap - lens/type · description · evidence · what clears it (the agent's to_clear) - not as prose bullets. A row with an empty evidence cell is an unfilled slot, not a shorter row.
    • Carry the DOMAIN_SKILLS_INVOKED value of both reviewer lenses - triageAgent, and designReviewAgent when it ran - on its own line, plus any Domain skills missed: line.
    • Close with the durable-async instruction, verbatim: "This is a durable async note - no reply needed now. Record the decision on this issue - run /milestone-feeder:remediate <n> to apply these findings, then clear the label - or re-run triage or solve-issue when ready." That line stays prose: it qualifies every row at once, so it has no cell to live in (skills/output-style.md, ## When prose is the correct form). Name the remediate verb unconditionally: it reads as an optional tool, so do not probe whether the feeder plugin is installed, and do not run it from here (skills/triage/SKILL.md#Non-negotiables).

    Example:

    🔴 Triage
    
    | Lens / type | Blocker | Evidence | What clears it |
    |---|---|---|---|
    | architect / contradiction | Recorded design is internally contradictory. | "mirror ConfirmImportPage grouping" (comment #1) vs "flat list, no collection picker" (comment #3) | Record the authoritative grouping decision before building. |
    | design / scalability | Flat 16-row list at realistic volume will produce a poor result. | Established grouped-card pattern at `Views/ConfirmImportPage.xaml` | Group under collection headers like ConfirmImportPage, or record a justified divergence. |
    
    This is a durable async note - no reply needed now. Record the decision on this issue - run `/milestone-feeder:remediate <n>` to apply these findings, then clear the label - or re-run triage or solve-issue when ready.
  3. Recommended-label routing - the label triage RECOMMENDS for this gap (returned in issueStates; the caller applies it):

    Gap typeRecommended label
    Any design/spec gap - architect contradiction / not-buildable / missing-criteria / risky-design, or any design-lens type (spec-insufficiency, scalability, pattern-inconsistency, missing-state, missing-affordance, accessibility)needs design
    A new dependency / non-design decision - architect undeclared-dependency, Blocker variant only (an Advisory never parks)needs decision

    Each parked issue carries exactly one triage-recommended label. With gaps of multiple types, select by precedence: needs design (any design or spec gap) wins; otherwise needs decision. Return that one label in issueStates.label (Step 7).

    blocked is not a triage recommendation: solve-milestone computes it at loop time from the dependency graph (Step 7) - an issue is blocked when an issue it depends on is not yet merged.

    ${CLAUDE_PLUGIN_ROOT}/skills/setup/SKILL.md Phase 4 defines the label colors and descriptions the caller uses.

triage does not apply labels, create branches, or open PRs. It posts the comment and returns the recommended label per blocked issue in issueStates (Step 7). The calling skill applies that label using the apply-time helper documented in skills/setup/SKILL.md Phase 4 (gh label create --force then gh issue edit --add-label), and leaves the issue open.

Step 6.5 - Cache write (best-effort)

After posting Step 6's comments, write/update entries for every freshly-triaged (MISS) issue. This step is best-effort: a write failure logs a warning and does not error the triage run.

  1. Build the entries object. One JSON object keyed by issue number; per freshly-triaged issue: triaged_at (this run's ISO 8601 timestamp) and result from the Step 4 aggregate. No key field - the script stamps it (item 2). The result object carries: blockers (boolean - false when Step 3.5 cleared them all; resolution text is never cached), label ("needs design" / "needs decision" / null), advisories (array of one-line strings), risk ("light" / "heavy"), and edges (the dependencyGraph.edges["<n>"] array for this issue).
  2. Hand it to the script. ${CLAUDE_PLUGIN_ROOT}/scripts/triage-cache.<sh|ps1> write <repo-root> <entries.json> <response.json> stamps each entry's change-signal key from that response using the same definition lookup compares against, re-reads the cache under the same resolution and degradation rules as Step 2.5, merges these entries over it one entry at a time, creates .milestone-config/, self-heals the committed .milestone-config/.gitignore so the cache is git-invisible from the first write, writes the canonical .milestone-config/triage-cache.json atomically, and removes the stale legacy root cache. Re-read, never reuse the Step 2.5 parse: that picks up a concurrent write instead of overwriting it. <response.json> is the saved Step 2.5 query keys response, never a fresh fetch and never the query edges one: that file predates the Blocker comments Step 6 just posted, and recomputing from it stores the pre-comment key - so the next run re-triages a blockered issue whose comment count has since changed ("Accepted trade-off" in Step 6). An absent or unparseable response is fail-open: the entries are written as supplied and the issue re-triages next run.
  3. Read the one record it prints. OK<TAB><path> when the cache was written; SKIP<TAB><reason> (no-jq, bad-entries, mkdir-failed, write-failed) when it was not. It always exits 0 - a SKIP sets the "cache write skipped this run" condition Step 5 reports, and never aborts the run.

Single mode: cache write applies identically - write the single issue's entry.

Step 7 - Return to the calling skill

Return to the invoking skill (e.g. solve-milestone, solve-issue) the following:

{
  dependencyGraph: {
    waves: [
      { wave: 1, issues: [A, B, C], parallel: true },
      { wave: 2, issues: [D], dependsOn: [A, B] },
      …
    ],
    edges: {
      "<n>": [<issue numbers this issue directly DEPENDS_ON>],
      …
    }
  },
  issueStates: {
    // **`issueStates` covers all issues** - both cache-HIT issues (populated from cached `result` via the Step 4 merge) and freshly-triaged MISS issues. Do not return only MISS-derived results.
    "<n>": { blockers: true | false, label: "needs design" | "needs decision" | null, clearLabel: true | false, advisories: ["<one-line advisory>", …], risk: "light" | "heavy" },
    …
  }
}

dependencyGraph.edges is the per-issue map: each key is an issue number (as a string), each value the array of issue numbers it directly depends on - preserved from the triageAgent DEPENDS_ON returns before wave aggregation. An issue with no dependencies has an empty array or is absent. The calling skill uses edges["<n>"] for per-issue buildability checks, not wave-level dependsOn, which is shared across all wave siblings.

blockers: true means the issue has at least one Blocker gap and is parked. label is the triage-recommended park label ("needs design" or "needs decision") when blockers: true; null when blockers: false, which means all-clear (Advisories are logged but not gating). The calling skill uses issueStates to decide which issues to build and which to hold, the label field to apply the park label via setup Phase 4's apply-time helper, clearLabel to remove one (skills/triage/blocker-resolver-dispatch.md (Unparking)), and separately derives blocked (and any transitive-dependent holds) from dependencyGraph.edges.

clearLabel defaults to false when a return omits it - only an explicit true removes a label.

risk is the per-issue classification computed in Step 4: "light" or "heavy", defaulting to "heavy" when inconclusive. solve-issue reads issueStates["<n>"].risk to resolve the build profile for that issue.

Severity → effect

SeverityEffect
BlockerParks the issue (triage comments + recommends the label; the caller applies it via setup Phase 4 and leaves it open); the loop continues with clean issues
AdvisoryLogged in the gap table and output; not gating; build proceeds

Output style

Read ${CLAUDE_PLUGIN_ROOT}/skills/output-style.md - this plugin's output contract. Its ## Terminal output section governs what this skill prints; its ## GitHub-facing prose, ## When prose is the correct form, and ## Evidence slots sections govern the 🔴 Triage comment this skill posts (Step 6). Plain English. Write every response, document, and GitHub issue, milestone, comment, and PR body in plain, concise English. Never include hypothesis, conjecture, or defensive text.

Read ${CLAUDE_PLUGIN_ROOT}/skills/citation-format.md - the one format every citation in those slots takes.

Non-negotiables

  • Authors no code, opens no PRs. The triage phase is read-only except for posting issue comments: it never edits a source file, creates no branch, opens no PR, applies no label, and moves no file. Triage performs no migration move: relocating a legacy root milestone-driver.json to .milestone-config/driver.json is owned by setup and solve-issue (the commands with a commit path; solve-milestone migrates via its dispatched build).
  • No interactive prompts. Blocker comments are durable async handoffs on the originating issue, never a mid-run pause awaiting a human reply.
  • No fabricated findings. Every gap cites its grounding (the exact recorded line, or file:line for a dependency). A claim ungroundable in the artifact is emitted as a Blocker ("cannot verify X from the issue/code"), never as a confident guess. If an issue cannot be retrieved, stop and report it; do not fabricate a stand-in.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.