Product Owner
/triage/merge #<issue>The Product Owner keeps the backlog groomed: it assigns priorities to un-prioritized issues and proposes duplicate groupings, either automatically (a daily schedule sweep, or immediately when a new issue is opened) or on demand via /triage. /merge #<issue> is its deterministic sibling — no LLM involved — for manually folding one issue into another.
Type: Hybrid — triage is LLM (Claude), merge is pure Bash · Scripts: .autoducks/agents/product/{pre,post}.sh (triage), .autoducks/agents/product/merge.sh (merge) · Verbs: triage, merge
flowchart LR
Triggers@{ shape: bolt, label: "Triggers\n(schedule, issues.opened,\n/triage, /merge)" }
Product["Product Owner"]
Backlog@{ shape: notch-rect, label: "Priorities set\nDuplicates closed" }
Triggers --triggers--> Product
Product -- grooms --> Backlog
class Triggers triggersOrange
class Product reviewTeal
classDef triggersOrange fill:#ffe8d4,stroke:#d66a28,color:#d66a28
classDef reviewTeal fill:#d4f4f8,stroke:#0e8a9c,color:#0e8a9c
Triggers
Section titled “Triggers”| Event | Condition |
|---|---|
| Schedule | product.schedule cron (default 0 9 * * *, UTC) — full backlog sweep |
| Issue opened | A newly opened issue (not a bot-created Task) — scoped triage of that one issue |
| Issue comment | /triage (full sweep, or scoped if commented on a specific issue), or /merge #<issue> |
| Workflow dispatch | mode: triage|merge, optional issue_number, dry_run |
Like the utility commands, the Product Owner sits outside the four-agent pipeline: no stage labels, no Definition-of-Ready cascade. It is also the only agent that fires without a human trigger — the schedule event carries no actor, so it bypasses the Authorization Gate by construction (there’s nothing to authorize: a scheduled run executes with the repository’s own access, not an untrusted commenter’s) rather than via an allow-list entry.
Definition of Ready
Section titled “Definition of Ready”None — like the other utility commands, /triage and /merge run against any issue and recompute their result from currently-visible ITS state rather than any stored plan.
Triage behavior
Section titled “Triage behavior”- Reacts 👀 and posts the status comment for a human-initiated run (a real
/triagecomment); an event-driven run (schedule,issues.opened) has no comment to react to, so it narrates through the job summary instead. - Gathers the inbox (
pre.sh): a single issue when scoped (issues.opened, or/triageon a specific issue), otherwise a full sweep of up toproduct.max_issues_per_runopen issues. For a sweep, a cheapits::search_issueskeyword pre-filter flags candidate duplicate pairs (dedup_candidates) as a hint — not a verdict — and every issue is annotated with whether it already carries a priority. - [LLM] Reads
/tmp/triage-inbox.md/.jsonand writes/tmp/triage-decisions.jsononly: proposedpriorities(Critical/High/Medium/Low, each with a rationale), sweep-only proposedduplicatesgroupings (canonical+duplicates+confidence+ rationale), and — whenproduct.provisional_classificationis enabled — a proposedFeature/Bugclassification hint for any issue that doesn’t yet carry one. Read-onlygit/ghexploration is permitted; mutatinggit/ghcommands andEditremain forbidden. post.shvalidates the decision file deterministically (core/robustness/validate-triage-decisions.py): enforces the named-priority vocabulary, drops duplicate groups belowproduct.confidence_threshold, caps closes atproduct.max_closes_per_run, and de-duplicates across groups so no issue is claimed twice. Malformed or rejected entries are dropped and reported in the job summary — never fatal to the run.- Applies the accepted priorities (skipping anything already prioritized or already closed) via
its::set_priority, and closes accepted duplicates viaits::close_issue(reasonnot_planned) — skipping any duplicate whose delivery phase has already started (aWork:*label or a live pipeline branch) — then leaves a cross-reference comment on each canonical issue naming what was folded in. - Applies the accepted classification, when present, as the
Bug/Featurelabel — the same label the Architect uses — which the Architect later confirms or overrides during design. Disable entirely withproduct.provisional_classification: false. dry_run: true(workflow-dispatch only) describes the proposal without applying anything.
Merge behavior
Section titled “Merge behavior”/merge #<issue> (or /merge <issue> — both forms are accepted) on issue N deterministically closes N as a duplicate of the target issue. There is no LLM step, so it runs instantly and never invokes the LLM provider.
- Reacts 👀, posts the status comment.
- Parses the target issue number from the comment; fails loudly on a missing target or a self-reference (
/merge #Ncommented onNitself). - No-ops if
Nis already closed — safe to re-run. - Refuses to merge if
N’s delivery phase has already started (aWork:*label or a live pipeline branch) — folding in an issue with in-flight work would abandon open task branches/PRs. Posts a comment pointing at/revertor/closefirst. - Comments on the target issue with
N’s body folded in, applies theDuplicatelabel, closesNasnot_planned, and best-effort cross-links the two as sub-issues.
Priority backends
Section titled “Priority backends”its::priority_backend / its::set_priority resolve one of three backends via product.priority_backend:
| Backend | Storage | Notes |
|---|---|---|
labels | The named Priority:Critical/Priority:High/Priority:Medium/Priority:Low labels | Works on any repository |
project | The repository’s linked Projects v2 board’s native single-select priority field | Projects-v2-only — requires a project linked to the repo; there is no classic-Projects or REST-API fallback |
off | Nothing | Priority assignment disabled; /triage still runs duplicate detection |
auto (the default) probes for a reachable Projects v2 priority field and resolves to project if found, else falls back to labels. See Configuration for the full product block, and Labels for exactly what the labels backend applies.
Overrides
Section titled “Overrides”/triage/merge #42/triage and /merge are a narrower command surface than the pipeline verbs: they accept only the bare verb (plus any custom alias configured under triggers.triage/triggers.merge) — model:/effort:/turns: directives and #auto: chaining are not parsed from the comment. model/effort/max_turns overrides for /triage are available only via workflow_dispatch inputs; /merge never invokes an LLM, so no override applies to it.