Skip to content

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
EventCondition
Scheduleproduct.schedule cron (default 0 9 * * *, UTC) — full backlog sweep
Issue openedA 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 dispatchmode: 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.

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.

  1. Reacts 👀 and posts the status comment for a human-initiated run (a real /triage comment); an event-driven run (schedule, issues.opened) has no comment to react to, so it narrates through the job summary instead.
  2. Gathers the inbox (pre.sh): a single issue when scoped (issues.opened, or /triage on a specific issue), otherwise a full sweep of up to product.max_issues_per_run open issues. For a sweep, a cheap its::search_issues keyword 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.
  3. [LLM] Reads /tmp/triage-inbox.md/.json and writes /tmp/triage-decisions.json only: proposed priorities (Critical/High/Medium/Low, each with a rationale), sweep-only proposed duplicates groupings (canonical + duplicates + confidence + rationale), and — when product.provisional_classification is enabled — a proposed Feature/Bug classification hint for any issue that doesn’t yet carry one. Read-only git/gh exploration is permitted; mutating git/gh commands and Edit remain forbidden.
  4. post.sh validates the decision file deterministically (core/robustness/validate-triage-decisions.py): enforces the named-priority vocabulary, drops duplicate groups below product.confidence_threshold, caps closes at product.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.
  5. Applies the accepted priorities (skipping anything already prioritized or already closed) via its::set_priority, and closes accepted duplicates via its::close_issue (reason not_planned) — skipping any duplicate whose delivery phase has already started (a Work:* label or a live pipeline branch) — then leaves a cross-reference comment on each canonical issue naming what was folded in.
  6. Applies the accepted classification, when present, as the Bug/Feature label — the same label the Architect uses — which the Architect later confirms or overrides during design. Disable entirely with product.provisional_classification: false.
  7. dry_run: true (workflow-dispatch only) describes the proposal without applying anything.

/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.

  1. Reacts 👀, posts the status comment.
  2. Parses the target issue number from the comment; fails loudly on a missing target or a self-reference (/merge #N commented on N itself).
  3. No-ops if N is already closed — safe to re-run.
  4. Refuses to merge if N’s delivery phase has already started (a Work:* 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 /revert or /close first.
  5. Comments on the target issue with N’s body folded in, applies the Duplicate label, closes N as not_planned, and best-effort cross-links the two as sub-issues.

its::priority_backend / its::set_priority resolve one of three backends via product.priority_backend:

BackendStorageNotes
labelsThe named Priority:Critical/Priority:High/Priority:Medium/Priority:Low labelsWorks on any repository
projectThe repository’s linked Projects v2 board’s native single-select priority fieldProjects-v2-only — requires a project linked to the repo; there is no classic-Projects or REST-API fallback
offNothingPriority 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.

/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.