Full Feature Request Triage
You are running a full triage of open feature requests for a WooCommerce Marketplace product. This orchestrator is built for products with hundreds of FRs: the working set lives on disk (not in your context), each phase runs in a subagent that returns a compact flagged list, and progress is resumable per FR so a disconnect or compaction never loses ground.
Read first:
.claude/skills/shared/RULES.md — display rules, confidence, plain text
comments, URL carry-through, HTML entity decoding, translation.
.claude/skills/shared/DISPLAY.md — pagination conventions (flat and
grouped).
.claude/skills/shared/RESOLVE_PRODUCT.md — product ID resolution.
.claude/skills/shared/KNOWLEDGE_BASE.md — used by the completed phase.
State directory: .triage-state/<productid>/ (gitignored). Helper CLI: scripts/triagestate.py. Use it for every read/write of state.
Step 1 — Resume or fresh start
After resolving the product (Step 2), run:
python3 scripts/triage_state.py status --product-id <id>
If exists is true and any phasestatus is inprogress or there are entries in flagged/ or actioned.jsonl, ask:
Found prior triage state for [product_name] (id [id]):
Fetched: [fetched_at humanized, or "never"]
Working set: [N] FRs cached
Actioned so far: [M]
Phase status: completed=[…] duplicates=[…] spam=[…] support=[…] stale=[…]
Resume from where you left off? (Y / start fresh / cancel)
- Y / Enter: continue. Skip phases already
completed. Re-enter
in_progress phases mid-flight.
- start fresh: run
triage_state.py reset --product-id <id>, then
proceed.
If no state exists, proceed straight to Step 2.
Display only the phases the user selected (Step 4). Don't list status for
phases that were never initialised.
Step 2 — Resolve the product
Follow .claude/skills/shared/RESOLVE_PRODUCT.md.
Step 3 — Verify ownership (read-only)
Fetch the first open feature request for this product:
wccom-feature-requests-list with product_id: <id>, status: "publish", per_page: 1
- If the list is empty, skip this check — nothing to action anyway.
- If at least one FR returned: read the response shape. If the API surfaces
an editable / canedit / links.edit field (or equivalent), use it to confirm write access without performing any write.
Do not perform a no-op write probe (the previous design wrote a current status back to the FR — even no-ops may trigger audit logs, webhooks, or last_modified bumps). If you cannot determine write access from the read response, proceed and let the first real write either succeed or fail; if the first write returns a permission error, stop and report:
⛔ Product [id] does not appear to be owned by the current API credentials.
Write actions are being rejected. Please re-run with credentials that own
this product.
Do not continue actioning further phases.
Step 4 — Select phases to run
Which checks should I run? (default: all)
[1] Completed — flag requests fulfilled by a shipped release
[2] Duplicates — find and close duplicate requests
[3] Spam — silently mark spam requests
[4] Support — close misplaced support questions
[5] Stale — close old low-vote requests
Reply with numbers (e.g. "2 4"), or press Enter / type A to run all.
Map to phase keys: completed,duplicates,spam,support,stale. Initialise:
python3 scripts/triage_state.py init \
--product-id <id> \
--product-name "<product_name>" \
--phases <comma-separated-keys>
init is resume-safe — preserves existing phasestatus and fetchedat.
Step 5 — Build the knowledge base (only if Completed is selected)
If Phase 1 (Completed) is selected AND knowledgebase.json does NOT exist under .triage-state/<id>/, follow .claude/skills/shared/KNOWLEDGEBASE.md in full to build the changelog + documentation knowledge base.
Write to .triage-state/<id>/knowledge_base.json per the schema in that file. Subsequent runs reuse it.
If Phase 1 isn't selected, or the file exists, skip.
Step 6 — Fetch ALL open feature requests (to disk)
Check freshness first:
python3 scripts/triage_state.py fetched-age --product-id <id>
fetched: false: proceed to fetch.
fetched: true: ask:
`` Cached working set is [age humanized] old ([N] FRs). Refresh from the API? (y / N — keep cache) ` - y: re-fetch. - Enter / N`: skip fetch, reuse cache.
To fetch — MUST be done in a subagent. Do NOT call wccom-feature-requests-list from the main (orchestrator) context. MCP tool results land in the caller's context window; for products with hundreds of FRs this blows the budget before any phase runs. The fetch subagent's context holds the raw JSON; the orchestrator only sees a compact summary.
Spawn a general-purpose subagent with the following prompt (substitute <id>, <product_name>):
You are a fetch-only subagent for the wccom triage orchestrator.
Goal: page through wccom-feature-requests-list and save every open FR to
disk for product <id> (<product_name>).
DO NOT:
- Call any wccom-feature-requests-* write tools.
- Re-fetch beyond what's needed (stop on the first short page).
- Echo FR payloads back to the orchestrator. Return only a compact summary.
DO:
- Call `mcp__wccom-feature-requests-prod__wccom-feature-requests-list` with
product_id: <id>, status: "publish", per_page: 100, starting at page 1.
- For each page, write the raw JSON response to
$TMPDIR/triage-page-<page>.txt via the Write tool. If a tool result is
truncated and saved to a tool-results file by the harness, copy that
file to $TMPDIR/triage-page-<page>.txt instead — do NOT Read it and
re-Write it.
- Stop when a page returns fewer than 100 items.
- Merge:
python3 scripts/save_frs.py $TMPDIR/triage-page-*.txt -o /Users/jasonkytros/wccom-triage-agents/.triage-state/<id>/working_set.jsonl
- Mark fetched:
python3 scripts/triage_state.py set-fetched --product-id <id>
- Return one-line JSON summary on stdout:
{"pages": <int>, "total": <int>, "working_set": "<path>"}
- Do NOT include FR titles, ids, or descriptions in the summary.
Working dir: /Users/jasonkytros/wccom-triage-agents
After the subagent returns, the orchestrator prints:
Fetched [total] open FRs for [product_name] → .triage-state/[id]/working_set.jsonl
If total == 0: report "No open feature requests found for [product_name]." and stop.
Never load the full working set into your own context. This includes:
- Do NOT call the MCP list tool directly from the orchestrator.
- Do NOT Read working_set.jsonl, page files, or actioned.jsonl in full.
- Do NOT Write FR payloads from the orchestrator (the payload appears in
tool-call args and is just as costly as a Read).
- Subsequent phases must always go through subagents.
Step 7 — Run each selected phase (subagent dispatch)
For each selected phase not already completed:
- Skip if done:
phase_status[<phase>] == "completed" → skip.
- Mark in progress:
`` python3 scripts/triagestate.py set-phase --product-id <id> \ --phase <phase> --phase-status inprogress ``
- Build unactioned input — drop FRs actioned in prior phases or earlier
this run: `` python3 scripts/triage_state.py filter-unactioned --product-id <id> \ --out .triage-state/<id>/flagged/<phase>.input.jsonl ``
- For duplicates only — prompt comparison mode before spawning (subagent
cannot prompt): `` Comparison depth: (F)ast keyword/TF-IDF, or (I)n-depth semantic rerank? In-depth needs .venv with sentence-transformers installed. ``
- Spawn a subagent — detection only, no writes. Use the subagent prompt
template below. Pass: - subagenttype: general-purpose - Skill file: .claude/skills/detect-<phase>/SKILL.md - Input JSONL: .triage-state/<id>/flagged/<phase>.input.jsonl - Output JSONL: .triage-state/<id>/flagged/<phase>.jsonl - Knowledge base path (completed only): .triage-state/<id>/knowledgebase.json - Comparison mode (duplicates only): fast | in-depth - mode: orchestrated (suppresses re-scan in PHASE_LOOP.md)
- Empty output handling — if the subagent's output JSONL is empty:
- Print: No <phase> findings. - Mark phase completed: `` python3 scripts/triage_state.py set-phase --product-id <id> \ --phase <phase> --phase-status completed `` - Continue to the next phase.
- Read flagged output — main agent reads only what it needs to present.
Page user prompts in batches of 20 per DISPLAY.md (grouped or flat per phase). Decode HTML entities. Translate non-English excerpts per RULES.md.
- Execute approved actions in the main agent (auth + confirmations stay
here). Per FR the user approves, see the per-phase action table below.
- **After every successful API write, append to
actioned.jsonl
immediately** — that's the resume anchor: `` python3 scripts/triagestate.py append-actioned --product-id <id> \ --phase <phase> --fr-id <id> --action <verb> --status <newstatus> ``
- API write failure handling — if any write call fails:
- Do not append to actioned.jsonl (so resume retries it). - Print: ⚠️ ID [id] — "[title]": failed — [error] - Continue to the next FR. - If a comment posted but the status update failed, log action=comment-only status=publish to actioned.jsonl so we know not to re-post the comment. Surface the half-state in the summary.
- Skipped FRs: when the user explicitly passes on a flagged FR (does
not approve closure), append to actioned.jsonl with action=skipped status=publish so future re-scans don't re-prompt: `` python3 scripts/triage_state.py append-actioned --product-id <id> \ --phase <phase> --fr-id <id> --action skipped --status publish ``
- End-of-phase loop:
- Ask: Would you like to action more requests from this phase? (Y / N) - Y: loop back to step 7 with remaining items. - N / empty: continue. - Always ask the re-scan prompt next. Substitute <phase> with the current phase name (e.g. "completed", "duplicates"): `` Run the <phase> detection again in case anything was missed, or move on to the next phase? (Another look / Move on) ` - Another look: re-run from step 3 (rebuild unactioned input, re-spawn subagent). If the subagent returns zero new flagged FRs, print No new findings., mark phase completed, and continue. Do not loop the re-scan prompt again. - Move on: mark phase completed`.
- Print a one-line phase summary, continue.
Per-phase action table
| Phase |
Comment? |
Status |
actioned.jsonl action |
| completed |
yes (changelog or docs template, see skill Step 7) |
completed |
marked-completed |
| duplicates |
yes (template by primary status, see skill Step 5) |
closed |
closed-duplicate |
| spam |
no |
spam → fallback closed |
marked-spam or marked-spam-fallback |
| support |
yes (support-redirect message, see skill Step 7) |
closed |
closed-support |
| stale |
yes (completed/standard/workaround variant per FR) |
completed or closed |
closed-stale or marked-completed |
Spam fallback — when calling wccom-feature-requests-update-status with status: "spam" returns a permission error (403 / "not allowed"), retry with status: "closed" and log action=marked-spam-fallback status=closed. Surface fallbacks in the phase summary.
Duplicates extras — after all closures for a primary, optionally run the "Extract insights for each primary" step from the duplicates skill (Step 6). Main agent only — it needs to read closed FR descriptions from the working set on disk.
Subagent prompt template
Use this when spawning the per-phase subagent. Substitute <…>.
Base (every phase)
You are a detection-only subagent for the wccom triage orchestrator.
mode: orchestrated
DO NOT:
- Call any wccom-feature-requests-* write tools (no comment, no update-status).
- Re-fetch the FR list.
- Resolve the product (already done).
- Crawl the changelog or docs (already done; knowledge base path provided
when relevant).
- Run the standalone PHASE_LOOP.md re-scan offer — the orchestrator handles
it. Return after the phase summary.
DO:
- Read the skill file at <skill_path>. Each detect-* skill marks where the
subagent should begin with a header line:
`## Step <N> — DETECTION STARTS HERE — …`
Skip everything above that header.
- Read the input working set from <input_path> (JSONL, one FR per line,
fields: id, title, description, votes, date, url, status).
- Process FRs in chunks of 50. After each chunk, append flagged records to
the output file <output_path>.
- Write flagged records as JSONL using the skill's "Flagged record schema"
block. Every record must include at minimum:
{ "id": <int>, "title": "<str>", "url": "<str>",
"reason": "<one sentence>", "confidence": "High" | "Low",
"excerpt": "<first 1–3 sentences of description, trimmed>" }
Include phase-specific extras per the skill's schema.
- Decode HTML entities in excerpts. Carry the API `url` field verbatim.
- Return a compact JSON summary to stdout when done:
{ "phase": "<phase>", "scanned": <int>, "flagged": <int>,
"output": "<output_path>", "notes": "<caveats or empty>" }
Inputs:
phase: <phase>
product_id: <id>
product_name: <product_name>
skill_path: <skill_path>
input_path: <input_path>
output_path: <output_path>
kb_path: <kb_path or "n/a">
comparison_mode: <fast | in-depth | "n/a">
Phase-specific tail — append to the base
completed:
- Use kb_path to load the knowledge base built by the orchestrator.
- Apply the docs source rule from KNOWLEDGE_BASE.md / detect-completed
Step 4: docs-sourced matches must cite a URL present in the knowledge
base; never infer.
duplicates:
- Skip Steps 1, 2, 2b, 2c of the skill — input is already on disk and
comparison_mode is set.
- Run the detection script directly on <input_path>:
- fast: python3 scripts/detect_duplicates.py <input_path> --json
- in-depth: .venv/bin/python3 scripts/detect_duplicates.py <input_path> --semantic --rerank --json
- Emit one record per duplicate-to-close with shared "group_id" and
embedded "primary" object per the duplicates schema.
- Add a top-level "groups": <int> field to the stdout summary.
spam / support / stale: base alone — no extra tail.
Step 8 — Triage summary
After all selected phases are completed, read state:
python3 scripts/triage_state.py status --product-id <id>
Group actioned.jsonl by action. Print:
## Triage Complete — [product_name]
Total open requests fetched: [N]
✅ Marked completed: [N]
❌ Closed as duplicates: [N]
🚫 Marked as spam: [N]
↳ closed (spam fallback): [N]
❌ Closed as support: [N]
❌ Closed as stale: [N]
⚠️ Failed (need retry): [N]
📋 Remaining open: [N − total actioned]
Step 9 — Free-form analysis (optional)
Ask:
Would you like me to run any further analysis on the remaining [N] open requests?
Examples:
- Flag requests that appear to be for a different plugin or product
- Identify requests in a specific language or region
- Surface requests around a particular feature area or theme
- Any other custom filter or pattern
Describe what you'd like me to look for, or type N to finish.
If declined: print "Triage complete. Thanks!" and stop.
9a — Run analysis
Build the unactioned set:
python3 scripts/triage_state.py filter-unactioned --product-id <id> \
--out .triage-state/<id>/flagged/freeform.input.jsonl
Spawn a general-purpose subagent. Pass the user's stated goal, the input path, and the output path .triage-state/<id>/flagged/freeform.jsonl. The subagent processes unactioned FRs in chunks of 50 and writes flagged records: {id, title, url, votes, date, reason, excerpt}.
Present findings paged in batches of 20 per DISPLAY.md:
## Free-Form Analysis: [user's stated goal]
[N] requests analysed · [Y] flagged (showing 1–20)
[N]. ID [id] — "[title]"
[votes] vote(s) · opened [date]
[url]
Reason: [one sentence]
Excerpt: "[1–2 verbatim sentences]"
9b — Action menu
Found [Y] requests matching your criteria.
What would you like to do with these?
1. What comment should I post? (describe or paste the comment text)
2. What status should I set? (closed / spam / completed / or leave open)
You can also select a subset — reply with numbers to act on specific items
(e.g. "1 3 5"), A for all, or N to skip.
9c — Execute
Plain text only (see RULES.md).
For each approved FR:
- If a comment was provided:
wccom-feature-requests-comment.
- If a status was provided:
wccom-feature-requests-update-status.
- On success, append to
actioned.jsonl with `phase=freeform
action=<verb> status=<new_status>`. On failure: report, do not append, continue.
Per-FR confirmation:
✔ ID [id] — "[title]": [action taken]
⚠️ ID [id] — "[title]": failed — [error]
9d — Loop
Anything else you'd like to analyse in the remaining [N] open requests?
(Describe what to look for, or N to finish.)
Repeat 9a–9d until done.
Step 10 — General rules
- Bash hygiene: when running inline Python via Bash, **always use a heredoc
with a single-quoted delimiter** — never python3 -c "...". The -c form passes the script through the shell, where ! triggers history expansion and $var references get expanded or stripped. A single-quoted heredoc suppresses all shell expansion: ``bash python3 << 'PYEOF' # safe: !=, $var, and ! all reach Python unmodified PYEOF ``
- Never re-fetch the FR list after Step 6. All analysis uses the cached
working set on disk.
- Never load the full working set into your own context. Always operate
on flagged outputs from subagents or paged reads.
- Always skip actioned FRs. Use
filter-unactioned before every
detection.
- Append to
actioned.jsonl immediately after every successful write.
- Wait for user confirmation before any write action.
- Process writes sequentially per FR (comment first, then status).
- Always record skips with
action=skipped status=publish so skipped
FRs don't reappear on resume or re-scan.
- Plain text comments only, ID display rules, URL carry-through,
HTML entity decoding, non-English translation — see .claude/skills/shared/RULES.md.