Skip to content

ARM-138: epic sub-tickets go to In Review, not Done - #78

Merged
stepandel merged 1 commit into
masterfrom
arm-138-epic-subticket-review
Apr 8, 2026
Merged

ARM-138: epic sub-tickets go to In Review, not Done#78
stepandel merged 1 commit into
masterfrom
arm-138-epic-subticket-review

Conversation

@agent-vera

@agent-vera agent-vera Bot commented Apr 8, 2026

Copy link
Copy Markdown

Problem

linear-epic was marking sub-tickets as Done the moment their commit landed on the shared epic branch — before the epic PR had even been reviewed, let alone merged. Humans then had to manually merge the PR separately, and because the tickets were already in the Done pile they fell off the radar.

Real-world evidence: ARM-79 epic — parent and all 5 sub-tickets (ARM-83…ARM-87) were marked Done, but the completion summary row for each sub-ticket literally said ✅ In Review next to the PR link. Humans then had to go merge the PRs.

Fix

Move epic sub-tickets to In Review on commit (not Done). They sit alongside the parent epic (which is also In Review after the PR opens) until the PR merges. Linear's built-in parent-cascade automation moves them to Done automatically when a human moves the epic parent to Done on merge — so no polling, no webhook, no new cron is needed.

Changes

All in .vera/skills/linear-epic/SKILL.md:

  1. Phase 3b step 6 (lines 116–118) — sub-ticket transition changed from Done to In Review. Rationale rewritten to explain the parent-cascade.
  2. Phase 1 filter rule (line 45) — execution queue now skips sub-tickets in In Review or Done. Without this, a crash-resume would re-process already-committed sub-tickets since they no longer hit Done.
  3. Phase 4 wrap-up trigger (line 213) — "All sub-tickets Done → move epic to In Review" became "All sub-tickets in In Review → move epic to In Review". The old trigger would never fire under the new flow.
  4. Edge-case table (line 229) — "All sub-tickets already Done" now accepts either In Review or Done, so an epic re-run after a partial human merge still skips to wrap-up.
  5. Completion summary template (lines 201–205) — example rows show ✅ In Review instead of ✅ Done. This matches what agents were already posting in practice.

Scope

The ticket's revised scope called for a two-line edit at lines 116/118. The additional follow-ons above are in the same file and are the minimal set required to keep the skill internally consistent — leaving only 116/118 changed would create a self-contradictory skill where the wrap-up trigger "all sub-tickets Done" would never match reality, and resumes would re-process completed sub-tickets.

Explicitly not in scope (per the ticket's scope revision):

  • linear-execute, linear-bug, pr-review-resolver, linear-review — unchanged
  • No new workflow, no new cron, no GitHub merge polling (Linear parent-cascade handles it)

Test cases

  1. grep for "straight to Done" in the file — zero hits.
  2. grep for stale sub-ticket.*Done references — all remaining hits are correct (either the filter rule that skips already-Done tickets, or the example execution plan showing a pre-existing Done sub-ticket being filtered).
  3. ✅ File reads end-to-end as internally consistent.

Closes ARM-138.

linear-epic was moving sub-tickets straight to Done the moment their
commit landed on the shared epic branch — before the epic PR had even
been reviewed, let alone merged. Humans then had to go merge the PR
separately, and because the tickets were already in the Done pile they
fell off the radar.

Move sub-tickets to In Review instead. They sit alongside the parent
epic (which is also already In Review) until the PR merges; Linear's
parent-cascade automation moves them to Done when the epic's state
transitions.

Also updates the downstream references in the same skill file that
depended on the old behavior so the skill stays internally consistent:

- Filter rule now skips sub-tickets in In Review *or* Done when
  building the execution queue (so resumes don't re-process
  already-committed sub-tickets).
- Epic wrap-up trigger now fires on "all sub-tickets in In Review"
  instead of "all sub-tickets Done" — otherwise the trigger would
  never fire under the new flow.
- Completion summary table template shows In Review instead of Done,
  matching what agents were already posting in practice (ARM-79).
- Edge-case row for "all sub-tickets already complete" accepts both
  states.
@linear

linear Bot commented Apr 8, 2026

Copy link
Copy Markdown
ARM-138 Don't mark tickets "Done" until PR is merged

Problem

Vera currently marks Linear tickets as Done when it opens a PR, not when the PR is actually merged. This creates two issues:

  1. Dual workflow — humans have to check the "Done" pile and then separately go merge the associated PRs. "Done" no longer means done.
  2. Forgotten PRs — once a ticket is in Done, it falls off the radar. PRs can sit unmerged indefinitely without anyone noticing.

Suggestion

Only transition tickets to Done when the linked PR is merged. While the PR is open and awaiting review/merge, the ticket should sit in an intermediate state (e.g. In Review or Ready for Merge).

Source

User feedback:

"Done" status is premature. Vera marks tickets as done when it pushes a PR, not when the PR is merged. This creates a dual workflow (check the "done" pile, then go merge) and risks PRs getting forgotten. Suggestion: only mark done on merge.

Notes for implementation

  • Audit all skills that set state to Done after opening a PR — at minimum: linear-execute, linear-bug, linear-epic, pr-review-resolver.
  • Decide on the intermediate state name (likely "In Review" if it exists in the ARM workflow).
  • Consider: who/what transitions the ticket to Done on merge? Options:
    • A cron/workflow that polls open PRs linked to In Review tickets
    • A GitHub webhook → Vera handler
    • Manual (human merges → human moves ticket — but this defeats the point)

Audit Findings

The ticket's framing ("Vera marks tickets as Done when it pushes a PR") is slightly off — the primary linear-execute flow already moves tickets to In Review, not Done. The real problems are:

  1. linear-epic marks sub-tickets Done on commit (before the epic PR is merged). This is the direct violation.
  2. Nothing transitions In ReviewDone on merge. Tickets accumulate in In Review indefinitely (currently 11 deep per the 2026-04-08 EOD report), defeating the whole point of the state.
  3. linear-bug has no explicit post-PR state transition — it leans on the top-level agent prompt's "move to In Review" default. Inconsistent with linear-execute.
  4. pr-review-resolver and linear-review do not move state — no change needed.

Per-skill audit

Skill File Current behavior Change needed
linear-execute .vera/skills/linear-execute/SKILL.md:84 Moves to In Review after PR opens ✅ None (optional: mention "Done happens on merge, via pr-merge-sync cron")
linear-bug .vera/skills/linear-bug/SKILL.md (Phase 6) No explicit state transition after PR — relies on agent-prompt default Add explicit "Move to In Review" step for consistency
linear-epic (parent) .vera/skills/linear-epic/SKILL.md:213 Moves epic to In Review after PR opens ✅ None
linear-epic (sub-tickets) .vera/skills/linear-epic/SKILL.md:116–118 Moves sub-tickets straight to Done on commit, before merge ❌ Change to In Review. The pr-merge-sync cron (below) will transition them to Done when the epic PR merges, via parent-traversal.
pr-review-resolver .vera/skills/pr-review-resolver/SKILL.md No state transitions None
linear-review .vera/skills/linear-review/SKILL.md No state transitions None

Real-world evidence: ARM-79 epic — parent and all 5 sub-tickets (ARM-83…ARM-87) were marked Done but the completion summary row for each sub-ticket literally says "✅ In Review" next to the PR link. The humans then had to manually merge the PRs. That's the bug.


Research Notes

Intermediate state name

The ARM team workflow has In Review (state type started) between In Progress and Done (type completed). Confirmed by: existing linear-execute/linear-epic skills using it, successful runtime updates in logs (e.g. .vera/logs/linear-*/… show {"action":"update","issueId":"ARM-137","state":"In Review"} succeeding), and the eod-report workflow listing 11 tickets in this state today. Use In Review — no new state to create.

How PRs are linked to Linear tickets

linear_issue action connect_pr creates a Linear Attachment on the agent session's issue. Attachments surface on linear_issue view as:

"attachments": {
  "nodes": [
    {
      "id": "...",
      "title": "fix(linear-issue): honor parent param on update action (ARM-128)",
      "url": "https://github.com/stepandel/anton/pull/76",
      "sourceType": "github",
      "createdAt": "2026-04-07T18:44:39.271Z"
    },
    { "sourceType": "githubCommit", "url": ".../commit/...", ... }
  ]
}

Filter predicate for "this is a PR attachment": sourceType === "github" and URL contains /pull/. Ignore githubCommit entries.

Branch-name-based auto-linking (arm-138-*) also creates attachments the same way.

Epic sub-tickets don't get their own PR attachment

linear-epic opens one PR for the whole epic and calls connect_pr with the epic's sessionId (line 170). Sub-tickets have no session of their own and no attachment. The cron has to find the PR by walking parent until it hits a ticket with an attachment. Implementation strategy is "parent traversal" — see workflow logic below.

GitHub API — check PR merge status

Single REST call per PR: GET https://api.github.com/repos/{owner}/{repo}/pulls/{number} → response has "merged": true|false and "state": "open"|"closed". pr-hygiene.yaml already has the exact jwt/installation-token scaffold for authenticated GitHub calls — copy it verbatim (steps get-token and analyze-prs). Quotas are fine: ~10–30 tickets per poll × 1 API call each = nothing close to the 5000/hr GitHub App limit.

Cron + workflow mechanics

  • .vera/workflows/*.yaml — YAML pipelines with tool / agent / approval steps. Approval steps are auto-stripped for cron runs.
  • .vera/crons/*.yaml — scheduler entries with type: workflow + workflow: <name> + inputs: {...}. Validated by python3 {baseDir}/scripts/validate_cron.py <name>.
  • Fresh scaffold: python3 {baseDir}/scripts/init_cron.py <name>.
  • Existing reference: eod-report.yaml cron → eod-report.yaml workflow.
  • Existing GitHub-polling reference: pr-hygiene.yaml workflow (don't reuse; it's interactive with an approval gate).

Webhook alternative — explicitly out of scope

grep -r webhook .vera/ finds zero webhook receivers in the harness repo. The GitHub webhook path would require adding a new webhook handler to stepandel/anton — materially larger change. Polling via cron is the simpler, in-scope answer and matches existing patterns. Mentioned in description as "Option 2" but this ticket implements Option 1 (cron poller) only.


Entry Points

File Role
.vera/skills/linear-epic/SKILL.md:116,118 Change "Move sub-ticket to Done" → "Move sub-ticket to In Review"; remove "do NOT move sub-tickets to In Review" line
.vera/skills/linear-bug/SKILL.md (end of Phase 6) Add explicit "Move ticket to In Review" step after opening PR
.vera/skills/linear-execute/SKILL.md:84 Optional clarifying note that "Done happens automatically when the PR merges"
.vera/workflows/pr-merge-sync.yaml NEW — polls In Review ARM tickets, transitions merged ones to Done
.vera/crons/pr-merge-sync.yaml NEW — schedules the workflow every 15 min
.vera/workflows/pr-hygiene.yaml (steps 1 + 3, lines 14–60 for the token/repo logic) Reference pattern for GitHub auth
.vera/crons/eod-report.yaml Reference pattern for cron→workflow wiring

Repo: the .vera/ config lives in stepandel/anton. A ready worktree exists at /workspace/worktrees/anton-arm-128 or create a fresh one as usual.


Definition of Done

  • Skill updates
    • linear-epic sub-tickets move to In Review on commit (not Done)
    • linear-epic parent still moves to In Review after PR opens (unchanged)
    • linear-bug Phase 6 has an explicit "Move to In Review" step
    • linear-execute has a one-liner clarifying Done happens on merge
  • New workflow .vera/workflows/pr-merge-sync.yaml
    • Fetches GitHub App installation token via the pr-hygiene JWT scaffold
    • Lists all In Review tickets for team ARM via linear_issue list
    • For each ticket, inspects attachments.nodes and picks the first entry where sourceType === "github" and URL matches /pull/
    • If no direct PR attachment, walks parent (up to 2 levels deep) and uses the parent's PR attachment
    • Calls GET /repos/{owner}/{repo}/pulls/{number} and checks merged === true
    • If merged, calls linear_issue update with state: "Done"; otherwise leaves the ticket alone
    • Writes a JSON report to .vera/workflows/runs/pr-merge-sync-report.json for debugging
    • Has a final summary step printing {scanned, moved_to_done, skipped_unmerged, skipped_no_pr, errors}
  • New cron .vera/crons/pr-merge-sync.yaml
    • schedule: "*/15 * * * *" (every 15 minutes)
    • type: workflow, workflow: pr-merge-sync, inputs: { team: "ARM" }
    • slack_channel omitted — agent posts to Slack only when it moved ≥1 ticket (agent-driven curl pattern from cron-jobs skill)
    • Validated with python3 {baseDir}/scripts/validate_cron.py pr-merge-sync
  • Verification
    • Manual dry-run: run_workflow pr-merge-sync against the live ARM board — prints the report, no false transitions
    • Manually open one small test PR, link to a throwaway ticket via connect_pr, move to In Review, merge the PR, re-run the workflow, confirm ticket transitions to Done
    • Epic sub-ticket path: confirm parent-traversal moves a sub-ticket whose parent has a merged PR
  • Documentation
    • PR description explains the new loop and the intentional 15-min latency
    • System-prompt line in linear-execute/linear-bug doesn't conflict with the new cron behavior

Test Cases

  1. Happy path, single ticket. Ticket in In Review has one GitHub PR attachment, PR is merged → workflow moves it to Done, report shows moved_to_done: 1.
  2. PR still open. Ticket in In Review has a PR attachment, PR is open (not merged) → workflow leaves it alone, report shows skipped_unmerged: 1.
  3. Epic sub-ticket. Sub-ticket in In Review has zero attachments; its parent epic has a merged PR attachment → parent-traversal finds the PR, workflow moves the sub-ticket to Done.
  4. Non-PR ticket. Ticket is in In Review for research (e.g. ARM-137) with no PR attachment and no parent → workflow skips it, report shows skipped_no_pr: 1. Does not move to Done.
  5. Closed but unmerged PR. PR was closed without merging → workflow leaves the ticket in In Review, logs a skipped_unmerged with a pr_closed_not_merged note. Human decides next step.

Edge Cases / Gotchas

  • Multiple PR attachments. Pick the most recently created sourceType: github entry with /pull/ in the URL. Do not merge-status-check every one — just the newest.
  • githubCommit attachments. Filter them out. They share sourceType: githubCommit and look like PR attachments otherwise.
  • Cross-repo PRs. Parse {owner}/{repo}/pull/{n} from the URL, don't hardcode stepandel/anton. Epic PRs land in army-control-plane; harness PRs land in anton.
  • Race with manual state moves. A human may move a ticket to Done independently; the cron's only transition is In Review → Done, so it never downgrades. Safe by construction.
  • Parent traversal depth. Cap at 2 levels (grandparent). Prevents infinite loops if someone creates a self-referential hierarchy.
  • Rate limits. linear_issue list --state "In Review" returns ~20 tickets typical. One GitHub GET per ticket. Total ~20 API calls per run, every 15 min → safely under all quotas.
  • Agent-prompt top-level instruction. The system prompt says "Move to In Review when your work is complete (PR opened)". This already matches the desired behavior and needs no change — but don't introduce a conflicting rule in the skills.
  • linear-epic wording cleanup. Lines 116 and 118 currently justify the Done transition with "since the work is verified and committed". That rationale no longer holds — update the comment too, not just the state string.

In-Flight Work Scan

  • In Progress (ARM): only ARM-138 itself.
  • In Review (ARM): 11 tickets, none touching .vera/skills/*, .vera/workflows/*, or .vera/crons/*. No file-level overlap.
  • Open PRs (stepandel/anton): none currently touch the four skill files or add new workflows/crons.
  • Branch off main. No prerequisite PRs.

Implementation Prompt

Update .vera/skills/linear-epic/SKILL.md:116,118 so sub-tickets move to In Review (not Done) on commit, add an explicit "Move ticket to In Review" step to the end of Phase 6 in .vera/skills/linear-bug/SKILL.md, then create .vera/workflows/pr-merge-sync.yaml (copy the JWT/installation-token bash scaffold from pr-hygiene.yaml steps 1+3 lines 14–60, enumerate ARM In Review tickets via linear_issue list, for each inspect attachments.nodes filtering on sourceType === "github" + /pull/ — walking parent up to 2 levels if empty, call GET /repos/{owner}/{repo}/pulls/{n} and transition to Done via linear_issue update when merged === true, writing a JSON report to .vera/workflows/runs/pr-merge-sync-report.json) and .vera/crons/pr-merge-sync.yaml (schedule: "*/15 * * * *", type: workflow, workflow: pr-merge-sync, inputs: { team: "ARM" }, no slack_channel — use agent-driven curl to notify only when ≥1 ticket transitioned); validate with python3 {baseDir}/scripts/validate_cron.py pr-merge-sync, dry-run the workflow against the live board, and open a PR in stepandel/anton with the issue identifier in the branch name.


Scope Revision (supersedes earlier prep output)

Human clarified: Linear's built-in automation already cascades parent → child state (when an epic moves to Done, its sub-tickets cascade automatically). No cron, no workflow, no GitHub polling is needed. The coding agent does not move tickets to Done at all — humans do that (or Linear's parent-cascade does it), after the PR is actually merged.

This reduces the ticket to a single two-line edit in linear-epic.

What actually changes

File: .vera/skills/linear-epic/SKILL.md

Line 116 — replace:

  1. Move the sub-ticket to Done using linear_issue (action: update, state: "Done").

with:

  1. Move the sub-ticket to In Review using linear_issue (action: update, state: "In Review"). The sub-ticket will transition to Done when the epic PR is merged and the parent epic moves to Done — Linear's parent-cascade automation handles it.

Line 118 — replace:

Do NOT open a PR per sub-ticket. Do NOT move sub-tickets to In Review — they go straight to Done since the work is verified and committed.

with:

Do NOT open a PR per sub-ticket. Sub-tickets sit in In Review alongside the parent epic until the epic's PR merges; Linear cascades them to Done automatically when the parent transitions.

What is NOT changing

  • linear-execute — already moves to In Review after PR opens ✅
  • linear-bug — relies on the top-level agent-prompt default ("Move to In Review when work is complete"), which matches the desired behavior ✅
  • pr-review-resolver, linear-review — don't transition state ✅
  • No new workflow, no new cron, no GitHub merge polling. The earlier "Option 1 (cron poller)" plan is dropped.

Definition of Done (revised)

  • linear-epic line 116 says sub-tickets move to In Review on commit
  • linear-epic line 118 rationale updated to mention Linear's parent-cascade
  • No other skill files changed
  • PR opened against stepandel/anton

Test cases (revised)

  1. Read .vera/skills/linear-epic/SKILL.md end-to-end — wording is consistent, no lingering references to "straight to Done".
  2. Re-check lines 210–215 (the epic parent transition to In Review) — unchanged, still correct.
  3. Sanity: the top-level system prompt's "In Review when work complete" rule still agrees with the epic skill.

Implementation prompt (revised)

Edit .vera/skills/linear-epic/SKILL.md:116,118 to change the sub-ticket state transition from Done to In Review, updating the surrounding rationale to explain that Linear's parent-cascade moves sub-tickets to Done automatically when the epic parent transitions on PR merge. No other files change. One PR in stepandel/anton.

@stepandel
stepandel merged commit 8349359 into master Apr 8, 2026
2 checks passed
@stepandel
stepandel deleted the arm-138-epic-subticket-review branch April 8, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant