Skip to main content
Version: 0.8.5

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 own SKILL.md gives it, sub-steps included (Step 3a, Step 5b). ensure_local_env.sh and Discovery & healthcheck run before step 1 and are unnumbered in the skill, so they are labelled Pre-step here, exactly as in phase 2. Phase 1 runs the same healthcheck but numbers it its step 1, so its diagram reads Step 1 — Discovery & healthcheck. The step and its name are identical across all three phases, and so is the statuscheck.sh call apart from its required --roleassigner, executor and reviewer respectively, 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 --list that resolves <PARENT-BRANCH>, the phase-check gh pr list, the per-PR 3a idempotency lookup, fetching PR diffs, step 4a's per-PR gh pr view --json state merge 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.sh resolves it as a self-call. On this call three of its four sources are live, in order: the parentbranch git config the assigner wrote in phase 1 — the phase-1 → phase-3 thread — then the issue's PR 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-key match 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.sh fetches 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, via pr_base.sh --parent-key. The parent is never re-fetched as an acting issue, fields.subtasks is 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 list whose 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.ps1 authenticates per-request as --role reviewer, picking that role's email:token pair out of jira-sdlc-tools.local.env for 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. See plugins/jira-sdlc/skills/_shared/project-config.md and plugins/jira-sdlc/skills/_shared/jira-api-reference.md §9.
  • <SELF> is resolved once per run — but only where a review happensgh api user --jq .login runs 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, gh is 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 recent APPROVED — → skip re-review and report the PR as already approved/awaiting merge, unless $ARGUMENTS asks for a re-review — that is the manual override for exactly this case. Most recent CHANGES REQUESTED — → a fresh re-review of the pushed fixes. This is the 3a idempotency node 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 --list globs 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 the prNumber that 3a needs.
  • Both verdicts go through --comment --body-file — in this plugin's default deployment the executor and reviewer share one gh account, 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 all there 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 prior APPROVED — 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): CONFLICTING means <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 via gh pr review --comment --body-file: approve → M-PARENT-READY (awaiting the human's manual merge), or request changes → M-PARENT-CHANGES-REQUESTED (integration file:line findings; 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 calls gh pr merge on 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.md caps 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 two M-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.md ends 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, a hotfix/ <PARENT-BRANCH> resolving to the staging base), the parent-PR-CLOSED case (5a), 5b's CONFLICTING stop, and the failure to resolve <SELF> (gh missing or logged out), which hands back the PR URLs instead.