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.
| Location | Role today |
|---|---|
plugin/skills/task-close-out/SKILL.md | Step 4 reads frontmatter, flips status: in-progress → closed/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.json | After T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics lands: declares prs:. |
plugin/skills/task-work/append_pr_url.py | After T-KPOU-task-work-writes-pr-url-to-prs-at-open lands: same edit pattern — share via reuse, not duplication. |
Proposed
Section titled “Proposed”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 touchingprs:. Commit message unchanged. - Append (drift):
prs:is missing or doesn’t contain the merged URL. Step 4’s commit also appends the URL toprs:(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.
Approach
Section titled “Approach”- Wait until T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics is merged on main
(recorded in
depends_on:). - Decide on shared-helper extraction. If T-KPOU-task-work-writes-pr-url-to-prs-at-open has
landed, factor its
append_pr_url.pylogic 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. - Edit
plugin/skills/task-close-out/SKILL.mdStep 4 to add the verify/append branch, with explicit prose for each of the three cases above. Preserve the existing autostash-rebase + push sequence — theprs:edit happens in the same staged change as the status flip. - Update the Step 4 commit-message template to conditionally include the addendum line. The completion_note body and PR citation are unchanged.
- 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. - 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).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/skills/task-close-out/SKILL.md | modify | Step 4 verifies/appends prs: against merged PR; commit body grows a conditional addendum line |
plugin/skills/task-close-out/ | new | optional: small frontmatter-edit helper if extraction from skill prose is cleaner |
plugin/skills/task-close-out/test_prs_verify.py | new | unit tests for the three verify/append/conflict branches |
plugin/lib/prs_field.py | new | optional: 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) |
Acceptance criteria
Section titled “Acceptance criteria”- 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 toprs:. Commit body does not include the addendum line. - AC-2: Close-out against a task whose
prs:is absent appends the merged PR URL asprs: [<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-workran post-Task-B closes out with exactly 3 main commits (claim, PR-open, close-out) andprs:populated with the merged URL. - AC-6:
validate_frontmatter.pypasses against every task file the close-out writes. - AC-7: All existing close-out tests pass; project quality checks pass.
Out of scope
Section titled “Out of scope”- 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.
Dependencies
Section titled “Dependencies”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.
Discovery context
Section titled “Discovery context”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.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-27. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
test_prs_verify.pytest_noop_when_url_already_presentexercises the happy path and asserts no addendum, file unchanged. - AC-2: auto —
test_create_when_prs_absentasserts the field is created and the addendum is emitted. - AC-3: auto —
test_append_when_prs_has_other_urlasserts 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.pyinvokesvalidate_frontmatter.pyafter mutation; covered bytest_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 checksOK 12/12 (baseline-gated)with 0 new findings.
What worked
Section titled “What worked”- 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, bothappend_pr_url.pyandverify_prs_against_pr.pyare 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.
Friction and automation gaps
Section titled “Friction and automation gaps”- The start_task.py cache-resolution bug surfaced last run (see
T-Y7QU-start-task-fails-loud-on-cache-miss) required the same
.sdlc/runtimesymlink 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_rootor start_task.py’s explicit walk. → T-Y7QU-start-task-fails-loud-on-cache-miss - The
quality_baseline.py --baseline-dirpath mismatch from the worktree (already tracked in T-5X6Y-task-work-step7-explicit-baseline-dir) bit again — passed--baseline-direxplicitly 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-closedto clear closed-task drift. → T-Y1JN-add-sdlc-reconcile-reporter
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-Y7QU-start-task-fails-loud-on-cache-miss — linked (existing): worktree-vs-main project-root resolution for start_task.py cache lookup
- T-5X6Y-task-work-step7-explicit-baseline-dir — linked (existing): pass —baseline-dir explicitly so the Step 7 gate isn’t a silent no-op in a worktree
- T-Y1JN-add-sdlc-reconcile-reporter — linked (existing): reconcile (or /sdlc:entities-migrate —include-closed) should surface closed-task schema drift
Depends on
Section titled “Depends on”T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics