Skip to content

T-TEJX-task-close-out-verifies-prs-against-merged-pr

Status: closed/done · Impact: medium · Complexity: small

Under slice 4a’s Shape C, close-out is the third and terminal main commit per task lifecycle. The Step 4 commit (docs(tasks): mark <basename> closed/done) should also verify the merged PR URL is in prs:. After T-KPOU-task-work-writes-pr-url-to-prs-at-open lands, this is a no-op for the happy path (/sdlc:task-work already wrote it). The append branch covers the drift case — a PR opened manually outside the protocol, or via the cross-repo skill — keeping reconcile’s record of “every closed task has its PR linked in prs:” true by construction.

LocationRole today
plugin/skills/task-close-out/SKILL.mdStep 4 reads frontmatter, flips status: in-progressclosed/done, writes completion_note:, clears relevance_note: and readiness_verified_at:, then stages + commits + pushes. Does not currently touch prs:.
plugin/skills/task-close-out/Skill directory; may grow a small frontmatter-edit helper to keep Step 4’s procedure consistent.
plugin/entities/task/schema.jsonAfter T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics lands: declares prs:.
plugin/skills/task-work/append_pr_url.pyAfter T-KPOU-task-work-writes-pr-url-to-prs-at-open lands: same edit pattern — share via reuse, not duplication.

Step 4 of /sdlc:task-close-out is extended to verify prs: includes the merged PR URL detected in Step 3. Three branches:

  • No-op (happy path, post-Task-B): prs: already contains the merged URL. Step 4’s commit makes the existing status + completion_note + readiness clearance edits without touching prs:. Commit message unchanged.
  • Append (drift): prs: is missing or doesn’t contain the merged URL. Step 4’s commit also appends the URL to prs: (creating the field if absent). Commit body adds an addendum: Appended merged PR <url> to prs: (was missing — likely opened outside /sdlc:task-work).
  • Conflict (unexpected): prs: contains URLs but none match the merged PR. Behavior: append the merged URL alongside the existing entries (don’t replace — historical PRs may be legitimate, e.g. a closed PR followed by a re-opened one). Same addendum text as the append branch.

Critically: the new logic does not add a second commit. It only extends the existing Step 4 commit (the one that flips status: to closed/done). The DONE pr=#<N> ... marker shape is unchanged.

  1. Wait until T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics is merged on main (recorded in depends_on:).
  2. Decide on shared-helper extraction. If T-KPOU-task-work-writes-pr-url-to-prs-at-open has landed, factor its append_pr_url.py logic into a shared module (e.g. plugin/lib/prs_field.py) and have both skills import it. If Task B has not yet landed, build the helper here and prepare Task B to consume it. Decision point: which lands first determines the location.
  3. Edit plugin/skills/task-close-out/SKILL.md Step 4 to add the verify/append branch, with explicit prose for each of the three cases above. Preserve the existing autostash-rebase + push sequence — the prs: edit happens in the same staged change as the status flip.
  4. Update the Step 4 commit-message template to conditionally include the addendum line. The completion_note body and PR citation are unchanged.
  5. Tests at plugin/skills/task-close-out/test_prs_verify.py (or as additions to an existing test file) covering: prs: contains URL → no addendum, no change to prs:; prs: missing → field created with URL, addendum present; prs: present with different URL → URL appended, addendum present; prs: empty array → behaves like missing.
  6. Update the close-out output contract in SKILL.md if the new addendum should surface in any marker (decision: it should not — the DONE pr=#<N> marker stays unchanged; the addendum lives in the commit message body only).
LocationKindChange
plugin/skills/task-close-out/SKILL.mdmodifyStep 4 verifies/appends prs: against merged PR; commit body grows a conditional addendum line
plugin/skills/task-close-out/newoptional: small frontmatter-edit helper if extraction from skill prose is cleaner
plugin/skills/task-close-out/test_prs_verify.pynewunit tests for the three verify/append/conflict branches
plugin/lib/prs_field.pynewoptional: shared module if T-KPOU-task-work-writes-pr-url-to-prs-at-open also lands its helper here (decided at implementation time based on land order)
  • AC-1: Close-out against a task whose prs: already contains the merged PR URL produces a Step 4 commit with the existing status + completion_note edits and no change to prs:. Commit body does not include the addendum line.
  • AC-2: Close-out against a task whose prs: is absent appends the merged PR URL as prs: [<url>]. The Step 4 commit body includes the addendum.
  • AC-3: Close-out against a task whose prs: contains other URLs (not the merged one) appends the merged URL (no replacement). The Step 4 commit body includes the addendum.
  • AC-4: The DONE pr=#<N> worktree=... branch=... marker shape is unchanged across all three branches.
  • AC-5: End-to-end happy path: a task whose /sdlc:task-work ran post-Task-B closes out with exactly 3 main commits (claim, PR-open, close-out) and prs: populated with the merged URL.
  • AC-6: validate_frontmatter.py passes against every task file the close-out writes.
  • AC-7: All existing close-out tests pass; project quality checks pass.
  • Detecting and recording multiple PRs per task at close-out. The PR detected in Step 3 is the most recent merged PR for the task branch. If multiple PRs exist (e.g. a closed PR followed by a re-opened one), only the merged one is verified. The append branch’s “alongside existing entries” semantics handle historical entries gracefully but don’t actively reconcile them.
  • Backfilling prs: for already-closed tasks. Same out-of-scope as Task B.
  • Detecting drift before close-out runs (e.g. emitting a warning if prs: doesn’t match the merged PR ahead of Step 4’s commit). The append behavior is fix-on-the-spot; observability for the drift case lives in reconcile.

Hard: T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics must be merged on main first (schema must declare prs:). Recorded in depends_on:.

Soft: T-KPOU-task-work-writes-pr-url-to-prs-at-open lands in parallel. Order doesn’t matter functionally; if Task B lands first, this task’s no-op branch is the common case. If this task lands first, every close-out exercises the append branch until Task B lands.

Slice 4a of T-FFHN-github-ref-leases-coordination, Shape C. Pairs with T-KPOU-task-work-writes-pr-url-to-prs-at-open: Task B writes at PR-open (the in-flight Obsidian affordance); Task C verifies at close-out (the historical record). Together they guarantee “every closed task has its PR linked in prs:” — a property reconcile can rely on as it grows new detectors.

Captured by /sdlc:task-work on 2026-05-27. PR: pending.

  • AC-1: auto — test_prs_verify.py test_noop_when_url_already_present exercises the happy path and asserts no addendum, file unchanged.
  • AC-2: auto — test_create_when_prs_absent asserts the field is created and the addendum is emitted.
  • AC-3: auto — test_append_when_prs_has_other_url asserts append semantics (existing entries preserved) and the addendum.
  • AC-4: agent-manual — confirmed by reading the updated SKILL.md Step 4 / Step 7: the DONE pr=#<N> worktree=... branch=... marker emission point is unchanged across the noop / create / append branches.
  • AC-5: agent-manual — the 3-commits invariant is preserved by Step 4 wiring (helper edits in-place, no new commit). End-to-end verification requires a future close-out run; the pinned invariant lives in plugin/skills/task-close-out/invariants.yaml.
  • AC-6: auto — verify_prs_against_pr.py invokes validate_frontmatter.py after mutation; covered by test_validates_after_mutation.
  • AC-7: auto — test_close_out_lease.py (17), test_append_pr_url.py (11), test_prs_verify.py (10), plugin/lib/prs_field/tests/test_core.py (14) all pass; quality checks OK 12/12 (baseline-gated) with 0 new findings.
  • The shared-module extraction (plugin/lib/prs_field/) was driven by the spec’s Approach step 2 and paid off: 14 core tests in one place, both append_pr_url.py and verify_prs_against_pr.py are thin policy wrappers, and a future contract change (multi-PR semantics, URL normalization) lives in one module rather than two.
  • The hotfix from PR #140 (refuse non-main branch in append_pr_url.py) carries over through the shared module — both helpers now share that safety property.
  • Baseline-gated quality checks correctly isolated the ~198 pre-existing drift items as informational; the gate fired only on new findings (0).
  • The symlink workaround for the worktree-as-project-root bug (pre-created right after git worktree add) avoided the start_task.py cache miss this time. The bug itself is still real and still tracked.
  • The start_task.py cache-resolution bug surfaced last run (see T-Y7QU-start-task-fails-loud-on-cache-miss) required the same .sdlc/runtime symlink workaround at worktree creation. The workaround is now muscle-memory — but it’s still a workaround. The real fix (skip the worktree’s own .sdlc/ when discovering project root) belongs in either the lease library’s _find_project_root or start_task.py’s explicit walk. → T-Y7QU-start-task-fails-loud-on-cache-miss
  • The quality_baseline.py --baseline-dir path mismatch from the worktree (already tracked in T-5X6Y-task-work-step7-explicit-baseline-dir) bit again — passed --baseline-dir explicitly to the sub-agent so it could re-run the gate. Same workaround, same friction. → T-5X6Y-task-work-step7-explicit-baseline-dir
  • The baseline showed 198 pre-existing findings vs Task B’s 0. Most are from the slice 4a schema-bump fixture churn; some may be from the dashboard merge. Worth surfacing in reconcile or in /sdlc:entities-migrate --include-closed to clear closed-task drift. → T-Y1JN-add-sdlc-reconcile-reporter

T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics


← Back to Tasks