Task Lifecycle — Phase 3: Review & aggregate approval
Phase 3 of the task lifecycle, run by the jira-task-reviewer skill.
Triggered from an issue's own worktree (branch-derived key, no key
argument) after every leaf executor has reported back and transitioned
its issue to <STATUS_IN_REVIEW>.
Scope is set by the worktree you stand in, and there are three entry
shapes, not two. From the parent's worktree the reviewer handles both
single-step top-level issues (no sub-tasks) and multistep parents (with
sub-tasks): for single-step it reviews the one PR and posts a final report
— no re-run needed (GitHub-for-Jira auto-transitions the issue to
<STATUS_DONE> on merge); for multistep it reviews each sub-task PR, finds
or creates the aggregate parent PR once sub-tasks are merged, and reviews
that too. From a sub-task's own worktree it reviews that sub-task's PR
and nothing else — no sibling, no parent PR, and no track determined; a
full pass is then a separate run from the parent's worktree. The reviewer
never merges anything — that remains the user's deliberate step. When a run
approved anything, its last step (7) asks whether to move those issues to
<STATUS_DONE> and transitions only what the user says yes to.
The diagram surfaces the two systems the reviewer drives as their own
swimlanes — GIT (anything that mutates or reads repo/PR state:
git fetch origin --prune, the anchored git branch -a --list that finds
<PARENT-BRANCH>, resolving this skill's own GitHub identity with
gh api user, the phase-check gh pr list, the per-PR 3a
idempotency lookup, fetching PR diffs, the step-4a per-PR merge-state
lookup, gh pr review --comment --body-file with APPROVED — / CHANGES REQUESTED — body-prefix
verdicts, and finding, mergeability-checking, or creating the aggregate
parent PR) and JIRA
(anything that mutates issue state: fetching the parent and then every
sub-task — the <STATUS_IN_REVIEW> ones become the PR set, but all statuses
are kept — reading fields.parent.key off a sub-task's own fetch to reach
its parent, each rejected issue → In Progress transition (a sub-task on the
multistep track, <PARENT-KEY> itself on single-step), multi-line comments on the reviewed
issue after every verdict, the per-sub-task summary comment on the
parent, every report comment posted on the parent, and the step-7
user-approved → Done transitions) — so the full
interaction reads User ↔ Reviewer ↔ GIT ↔ JIRA left to right. The two
pre-flight scripts (ensure_local_env, statuscheck) and pr_base.sh are
drawn as self-calls because they neither mutate GIT nor JIRA state — not
because they stay local. Only ensure_local_env does: the
healthcheck probes both credentials over the network (GET /myself for
Jira, GET /repos/… for GitHub), and pr_base.sh's second source is the
issue's own PR target branch: Jira comment. Read-only of those two
systems, not of the machine: statuscheck.sh also re-logs gh in from the
stored PAT (gh auth logout, then gh auth login --with-token), a
deliberate local mutation that exists so a stale read-only token can't 403 at
gh pr create later.
Sequence diagram
What the diagram shows
- Step labelling — every node carries the step number
jira-task-reviewer's ownSKILL.mdgives it, sub-steps included (Step 3a,Step 5b).ensure_local_env.shand Discovery & healthcheck run before step 1 and are unnumbered in the skill, so they are labelledPre-stephere, exactly as in phase 2. Phase 1 runs the same healthcheck but numbers it its step 1, so its diagram readsStep 1 — Discovery & healthcheck. The step and its name are identical across all three phases, and so is thestatuscheck.shcall apart from its required--role—assigner,executorandreviewerrespectively, since auth is role-scoped and the script has no default credential. Only each skill's own numbering differs, and the diagrams follow the skill rather than renumbering it. - Two tracks, three entry shapes, one skill — from the parent's
worktree, step 1 determines the track from
fields.subtasks: empty → single-step (the PR set is the one parent PR), non-empty → multistep (the PR set is each In Review sub-task PR). Each track walks its own branches from the same phase check. The third entry shape — a run from a sub-task's own worktree — determines no track at all: its PR set is that one sub-task's PR, and it never reaches the phase checks, step 4 or step 5. In all three the review-loop body (step 3, including its 3a idempotency check) is identical — only the PR set and the post-loop outcomes differ. - Participant routing — the reviewer orchestrates three parties.
GIT owns repo/PR state: the opening fetch, the anchored
git branch -a --listthat resolves<PARENT-BRANCH>, the phase-checkgh pr list, the per-PR 3a idempotency lookup, fetching PR diffs, step 4a's per-PRgh pr view --json statemerge check, the verdict comment (gh pr review --comment --body-file), and finding or creating the aggregate parent PR. JIRA owns issue state: fetching the parent + sub-tasks (every sub-task is fetched and every status kept;<STATUS_IN_REVIEW>only selects the PR set), climbing from a sub-task branch to its parent, each rejected issue → In Progress transition with its findings comment — a sub-task on the multistep track,<PARENT-KEY>itself on the single-step track — and the summary/report comments posted on the parent after every review.<BASE_BRANCH>sits in neither lane:pr_base.shresolves it as a self-call. On this call three of its four sources are live, in order: theparentbranchgit config the assigner wrote in phase 1 — the phase-1 → phase-3 thread — then the issue'sPR target branch:Jira comment, then the env default (sources 1, 2 and 4). The one it skips is source 3, the parent-branch search, which is gated on--parent-key— and this call deliberately never passes it. That gate gets skipped in both directions at once: a--parent-keymatch resolves before the env default is ever consulted, so omitting the flag is both what keeps that default reachable and what stops<BASE_BRANCH>resolving to<PARENT-BRANCH>. Phase 2 draws the same script the same way. - A sub-task branch does not climb into a full pass — the reviewer
derives the key from the current branch (feature/
-slug or hotfix/ -slug) and jira.shfetches that issue. A top-level issue with no sub-tasks follows the single-step track. If the issue is a Subtask, the run's scope stays that one sub-task's PR:fields.parent.key(already on the sub-task's own fetch) is read only to resolve<PARENT-BRANCH>, the base its PR targets, viapr_base.sh --parent-key. The parent is never re-fetched as an acting issue,fields.subtasksis never read, no track is determined, and the parent PR is never touched — that sweep belongs to a separate run from the parent's own worktree. Its walk is step 2 (for this one sub-task) → 3 → 6 → 7. - Phase check first, and track-aware — an explicit GIT
gh pr listwhose return dispatches the top-level branches. On the single-step track it has three outcomes: no PR → report that the executor hasn't opened one yet and exit (nothing to review); an open PR → the step-3 review loop; a merged PR → the S-MERGED report and exit. On the multistep track both the no parent PR and the open parent PR outcomes split further on the sub-task statuses, because neither state means what it looks like on its own: every sub-task<STATUS_DONE>→ the merges have happened, so skip to step 5 (the only place the parent PR is ever created, and where 4a's "merge them manually, then re-run" lands); any sub-task not yet<STATUS_DONE>→ the work is still in flight, so run the full step-2 pass and leave the parent PR to a later run. A merged parent PR → the M-FULLY-COMPLETE report and exit. Both phase checks also share two rules: several PRs back → act on the OPEN one (several open → ask which), and a CLOSED, unmerged PR matches no enumerated state, so stop and ask — someone abandoned that branch's PR deliberately, and both opening a replacement and reviewing a dead one would be guessing at intent. Every merged-state exit posts the step-6 report only — GitHub-for-Jira already handled<STATUS_DONE>, so there is no wrap-up to take and step 7 has nothing to offer. - The reviewer carries its own Jira identity on every call — there is no
login step to run first.
jira.sh/jira.ps1authenticates per-request as--role reviewer, picking that role'semail:tokenpair out ofjira-sdlc-tools.local.envfor the one call, so every canonical report and every reject-path transition below is attributed to the reviewer's Jira account. Nothing is stored and no account is "active", which is why a reviewer run can overlap a phase-2 executor run without either one displacing the other's credentials. Note this is the reviewer's Jira identity; its GitHub identity is a separate thing, and the one that matters for the idempotency check below. Seeplugins/jira-sdlc/skills/_shared/project-config.mdandplugins/jira-sdlc/skills/_shared/jira-api-reference.md§9. <SELF>is resolved once per run — but only where a review happens —gh api user --jq .loginruns at the top of step 3, before the review loop, and its value is substituted literally into every 3a check. That is why the diagram draws it inside the branches that review something rather than once up front: the exits that review nothing — no PR yet, S-MERGED, M-FULLY-COMPLETE as the step-1 phase check detects it, and the multistep nothing-to-review case — never reach step 3 and never resolve it. M-FULLY-COMPLETE's second detector is the exception: a 5a that finds the parent PR already merged is reached through the review loop (step 2 → 3 → 4a → 5a), so<SELF>is already resolved by the time that exit is taken. Two branches review without entering step 3, and both are the multistep phase check's every-sub-task-<STATUS_DONE>split: the open parent PR path, which jumps from step 1 straight to 5b, and the no parent PR path, which jumps from step 1 to 5a — where the parent PR is created — and on into 5b. Neither passes through step 3, so 5b resolves<SELF>itself on both. That is what the skill's 5b means by "the all-sub-tasks-merged re-run path": one class, two routes. If that call errors,ghis missing or logged out: the run reports the error and hands the user the PR URLs rather than proceeding.- Idempotent review (step 3a) — before every PR review — the
single-step PR, each multistep sub-task PR, and the aggregate parent PR
(5b) — the reviewer checks whether its own GitHub identity already left
a verdict comment (matched on author + body prefix, never on review
state— both verdicts land as comments, so this identity leaves no review state to key on). The most recent verdict wins: a later one supersedes an earlier, which is what makes an approve→regression→reject sequence readable. Most recentAPPROVED —→ skip re-review and report the PR as already approved/awaiting merge, unless$ARGUMENTSasks for a re-review — that is the manual override for exactly this case. Most recentCHANGES REQUESTED —→ a fresh re-review of the pushed fixes. This is the3a idempotencynode drawn in each review branch. - Empty exits and flag-and-skip (step 2) — the reviewer first discovers
each In Review sub-task's branch (anchored
git branch -a --listglobs that also strip git's+marker for a branch checked out in another worktree) and its open PR —gh pr list --head <subtask-branch> --base <PARENT-BRANCH> --state open. A sub-task with no branch or no open PR is flagged and skipped, not reviewed; more than one open PR is a question for the user, the same rule the phase checks carry. If zero sub-tasks have an open PR, the run reports that there is nothing to review and exits before the review loop — except on the multistep track, where the phase check's split applies first: every sub-task<STATUS_DONE>means the PRs are merged and only the parent PR is missing, so the run goes to step 5 instead of exiting. Step 2 also runs on the sub-task-worktree path, for that one sub-task alone — it is the only place a branch becomes theprNumberthat 3a needs. - Both verdicts go through
--comment --body-file— in this plugin's default deployment the executor and reviewer share oneghaccount, and GitHub blocks an author from approving or requesting changes on their own PR. Both verdicts are recorded as review comments with the decision in their body prefix (APPROVED — …/CHANGES REQUESTED — …). The Jira transition to<STATUS_IN_PROGRESS>(reject path) is the actual workflow gate; the GitHub comment records findings and makes the verdict machine-detectable by the idempotency check (step 3a). - Single-step is one-and-done — on the single-step track, approval
posts the final report immediately (S-APPROVED outcome, step 6), then
step 7 offers to close the issue. Decline and GitHub-for-Jira
auto-transitions it to
<STATUS_DONE>when the user merges instead; either way no reviewer re-run is required. Only the S-CHANGES-REQUESTED outcome (reject) needs a re-run after fixes. - Single pass, no merge cascade — each In Review sub-task PR is reviewed in order, one at a time. The verdict happens immediately per-PR. There is no separate "batch merge" step; approved PRs are left for the human to merge manually, and rejected items keep the loop going so the full state is known. The only thing that stops the loop is running out of sub-task PRs.
- Continue on rejection, don't stop — when a PR fails the review,
the reviewer posts the
CHANGES REQUESTED —verdict comment, transitions that issue back to<STATUS_IN_PROGRESS>, records it as blocked, and continues to the next sub-task. The final report at the end (step 6) lists both approved and rejected items so the fix-and-re-run cycle is clear. - One report body, posted to every destination that needs it — 3d, 3e,
5b and 6 all emit the same canonical review report, filled from
_shared/templates/review-report.md. On the multistep track 3d writes one body to a temp file, posts it to GitHub and to<SUBTASK-KEY>, and 3e posts that identical file again to<PARENT-KEY>— a fresh comment per sub-task, which is the intentional audit trail, not a one-line tally. On the single-step track 3d's Jira destination and step 6's are both<PARENT-KEY>, so 3d posts only the GitHub half and 3e is skipped entirely: step 6 is that run's single Jira record. The same reasoning skips 3e on the sub-task-worktree path. - Parent PR: review, approve or reject, still never merge (multistep
only) — once every sub-task PR is merged, the reviewer finds or creates
the aggregate parent PR (GIT).
gh pr list --state allthere returns four states, not three: if that PR is already CLOSED it stops and lets the user decide (5a) — it never reopens or recreates it; if it is already MERGED, 5a is the second detector of M-FULLY-COMPLETE, after the step-1 phase check. Otherwise 5b runs the 3a idempotency check first — a priorAPPROVED —self-review means the parent PR is already reviewed, so the run reports it as awaiting manual merge and posts nothing to GitHub. Past that it asks GitHub whether the PR is mergeable (gh pr view --json mergeable,mergeStateStatus):CONFLICTINGmeans<PARENT-BRANCH>has fallen behind<BASE_BRANCH>, so it stops and reports rather than reviewing a diff that cannot merge — this is the only detector the "parent branch is behind its base" edge case has. Past that it reviews the lighter aggregate diff (integration focus) and records a verdict on the parent PR viagh pr review --comment --body-file: approve → M-PARENT-READY (awaiting the human's manual merge), or request changes → M-PARENT-CHANGES-REQUESTED (integrationfile:linefindings; fix on<PARENT-BRANCH>and re-run). Unlike a sub-task reject, the parent reject does not transition any Jira issue — the aggregate<PARENT-KEY>stays as-is. The reviewer never callsgh pr mergeon the parent — merging into<BASE_BRANCH>is the human release decision. After the parent PR merges, no re-run is required; a re-run only reports the already-merged state (M-FULLY-COMPLETE). - No automatic status transitions — the reviewer never moves an issue to Done on its own. The only transition it performs unprompted is a rejected issue → In Progress, and only on a sub-task reject (3d) or a single-step reject (3d) — never on the multistep parent-PR reject (5b), where no executor will pick the parent branch up anyway.
- Done is offered, never assumed (step 7) — the run's last step names every issue it approved and asks once whether to close them, moving only what the user confirms. It asks because an approval is not a merge: the reviewer never merges, so those PRs are still open, and on boards where Done means merged the card would be jumping ahead of the automation that really closes it. Boards that close at approval want the opposite, and nothing in the repo says which kind this project is. Declining (or a non-interactive run) leaves everything as-is and GitHub-for-Jira's merge automation closes the issues later. The already-merged exits (S-MERGED, M-FULLY-COMPLETE) skip the question — those issues are Done already. No extra Jira comment is posted either way: the step-6 report is still the run's single final comment.
- A branch posts a JIRA report comment on the parent exactly when it
renders a catalogue outcome — that is the rule, and it decides every
branch rather than a list you have to keep in sync. Step 6 sends the
report to chat and to
<PARENT-KEY>as a single Jira comment, and step 6 must "pick exactly one outcome from the catalogue", which_shared/templates/review-report.mdcaps at ten blocks. So those ten are the whole of it: the single-step approve/reject (S-APPROVED / S-CHANGES-REQUESTED) and merged (S-MERGED) reports, M-ALL-APPROVED and M-SOME-BLOCKED, the parent approve (M-PARENT-READY) — which the 5b already-approved skip also renders, posting no GitHub comment but still reporting — the parent reject (M-PARENT-CHANGES-REQUESTED), the merged-state report (M-FULLY-COMPLETE), and the twoM-SUBTASK-*blocks, which step 6 renders at run level only on the sub-task-worktree path. Every other branch surfaces its state to the user in chat and waits, posting nothing to Jira, for one reason: no block exists for it to fill. That covers the single-step no-PR-yet exit and the multistep nothing-to-review exit —SKILL.mdends the first with a chat line and a bare "Exit." at the phase check, the second with "report and exit" at step 2, neither reaching step 6 — the phase checks' CLOSED, unmerged stop, step 1's own stop-and-asks (zero or several branch matches,source=unresolved, ahotfix/<PARENT-BRANCH>resolving to the staging base), the parent-PR-CLOSED case (5a), 5b'sCONFLICTINGstop, and the failure to resolve<SELF>(ghmissing or logged out), which hands back the PR URLs instead.