T-KPOU-task-work-writes-pr-url-to-prs-at-open
Status: closed/done · Impact: medium · Complexity: small
Under slice 4a’s Shape C, the second of three main commits per task lifecycle lands at PR-open: the
task’s prs: field grows by one URL when gh pr create returns. This is the Obsidian visibility
win — a reader looking at the task file in the main repo sees the live PR without shelling out to
GitHub. Closes the writer side for the PR-open transition; the existing lease-ref transition to
awaiting-review is unchanged.
| Location | Role today |
|---|---|
plugin/skills/task-work/SKILL.md | Step 10 opens the PR via gh pr create and transitions the lease to awaiting-review. Does not touch main. |
plugin/skills/task-work/start_task.py | Step 5b’s frontmatter-edit-on-main helper; canonical pattern for “edit task file + commit + push —autostash” from within a worktree. |
plugin/entities/task/schema.json | After T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics lands: declares prs: as an optional array of URL strings. |
plugin/scripts/sdlc_lease.py | Lease CLI; Step 10’s existing lease transition runs through it (task transition --phase awaiting-review). |
Proposed
Section titled “Proposed”A new helper at plugin/skills/task-work/append_pr_url.py performs the frontmatter edit + commit +
push cycle for a single PR URL. The script is idempotent: if the URL is already in prs:, it exits
with a NO-OP url=<URL> marker and no commit. If prs: is absent, it creates the field. If
present, it appends without duplicating. The commit message follows the conventional pattern
docs(tasks): record PR for <basename>. /sdlc:task-work Step 10 calls this helper immediately
after gh pr create returns and before the lease transition to awaiting-review, so a crash
between PR-open and lease-transition leaves a recoverable state (lease ref still working,
frontmatter records the PR — reconcile already flags prs: present + lease still in working).
Approach
Section titled “Approach”- Wait until T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics lands on main.
prs:must be a declared property before the helper can write to it (orvalidate_frontmatter.pyrejects the result). - Build
plugin/skills/task-work/append_pr_url.py:- Signature:
append_pr_url.py <task-file> <pr-url> [--no-commit](the--no-commitflag is for tests). - Reads frontmatter (reuse the same YAML round-trip helper
start_task.pyuses — find via grep). - Appends
pr-urltoprs:if absent; emitsNO-OP url=<url>to stdout and exits 0 if already present. - On change: writes the file, stages it, commits with the conventional message + heredoc body,
runs
git pull --rebase --autostash, thengit push. - Emits
WROTE url=<url> commit=<short-sha>on success.
- Signature:
- Wire into
plugin/skills/task-work/SKILL.mdStep 10: after thegh pr create --json urlcall returns, invokeappend_pr_url.py <main-repo-path>/docs/planning/tasks/<basename>.md <url>before the lease transition. - Tests at
plugin/skills/task-work/test_append_pr_url.pycovering: missing prs: field (creates it), existing prs: with same URL (no-op), existing prs: with different URL (appends), malformed prs: (errors clearly), invalid task-file argument (errors clearly). - Update the Step 10 stdout markers documented in SKILL.md so callers (the orchestrator) see the new state.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/skills/task-work/append_pr_url.py | new | helper script: frontmatter edit + commit + push for a single PR URL; idempotent |
plugin/skills/task-work/test_append_pr_url.py | new | unit tests covering missing-field, idempotent, append, and error paths |
plugin/skills/task-work/SKILL.md | modify | Step 10 calls the new helper after gh pr create, before lease transition |
plugin/skills/task-work/start_task.py | modify | factor out shared frontmatter-edit helpers if needed (only if reusable extraction is clean — otherwise leave alone) |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Running
append_pr_url.py <task-file> <url>against a task with noprs:field writesprs: [<url>], stages and commits the file with messagedocs(tasks): record PR for <basename>, and pushes. Stdout:WROTE url=<url> commit=<sha>. - AC-2: Running the same command again with the same URL exits 0 with
NO-OP url=<url>on stdout; no commit is created, no push fires. - AC-3: Running with a different URL on a task that already has one in
prs:appends it (yielding a 2-element list); the order matches the temporal order of PR opens. - AC-4:
/sdlc:task-workStep 10 invokes the helper aftergh pr create. End-to-end run against a fresh task produces three commits on main: claim (Step 5b), PR-open (new), close-out (Step 4 of/sdlc:task-close-out). - AC-5: If
append_pr_url.pyis interrupted between commit and push, a re-run finishes the push without duplicating the commit (the local commit is still there; the helper detects and skips the edit step). - AC-6:
validate_frontmatter.pypasses against every task file the helper writes. - AC-7: All existing tests pass; project quality checks pass.
Out of scope
Section titled “Out of scope”- Backfilling
prs:for tasks whose PRs opened pre-cutover. Migration of historical state is intentionally not part of slice 4a. - The close-out append/verify path — that’s T-TEJX-task-close-out-verifies-prs-against-merged-pr.
- Changing the lease transition logic. The existing
task transition --phase awaiting-reviewcall is unchanged; the new step just runs ahead of it. - Cross-repo task PRs (the
cross-repo-task-prskill). If that skill opens a PR, its writer-side handling is a follow-up task; this one only covers/sdlc:task-work.
Dependencies
Section titled “Dependencies”Hard: this task cannot start until T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics
is merged on main (the schema must declare prs: first or validate_frontmatter.py rejects the
writer’s output). Recorded in depends_on:.
Discovery context
Section titled “Discovery context”Slice 4a of T-FFHN-github-ref-leases-coordination, Shape C. The user’s framing during the
slice-4a design discussion: “we should write out a PR after task-work right?” — yes, this task is
what makes that true. Without this writer, prs: would only appear at close-out, leaving in-flight
tasks with no PR link visible in Obsidian — defeating the affordance.
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_append_pr_url.pycase_missing_prs_field_creates_and_commitsexercises the missing-prs:happy path against a sandbox repo and asserts the commit message + stdout marker shape. - AC-2: auto —
case_same_url_is_noopasserts the second invocation against an unchanged file emitsNO-OP url=<url>and creates no commit. - AC-3: auto —
case_different_url_appends_in_orderasserts append semantics and the resulting 2-element list ordering. - AC-4: agent-manual —
plugin/skills/task-work/SKILL.mdStep 10 was wired with the new sub-step; the parent will exercise the full claim → PR-open → close-out cycle once this PR merges and Task C lands. The end-to-end 3-commits-per-task assertion is observable fromgit log origin/main --onelineafter a future task lifecycle completes (the auto check moves there). - AC-5: auto —
case_crash_recovery_noopsimulates “commit landed locally, push didn’t” by rolling back the origin and asserts the second run is a NO-OP (URL already present inprs:). - AC-6: auto —
validate_frontmatter.pyis invoked insideappend_pr_url.pybefore the commit; every test that exercises the commit path implicitly covers this. - AC-7: auto —
run_quality_checks.py --diff-against-baselinereportsOK 12/12against the captured baseline.
What worked
Section titled “What worked”- Reusing
start_task.py’s YAML round-trip +mktemp+git commit -Fpattern made the helper script’s shape feel idiomatic against the existing skill code. No new dependency surface. - The
--no-commitflag carried its weight in tests — file-edit-only assertions don’t need a sandboxgit init, so the test file stays readable. - Baseline-gated quality checks correctly identified pre-existing drift as informational; the gate only fired on the 0 new findings this branch introduced.
Friction and automation gaps
Section titled “Friction and automation gaps”start_task.pycache-resolution treats the worktree’s own.sdlc/skill-ext/as the project root and looks for.sdlc/runtime/lease-cache/inside the worktree instead of the main repo. Thechore(tasks): startcommit landed but the CAS-REPLACE to phase=working silently no-op’d. Worked around mid-flight by symlinking<worktree>/.sdlc/runtime/→<main>/.sdlc/runtime/. Fix:_find_project_rootshould treat the.sdlc/worktrees/<basename>/ancestor specially (skip the worktree’s own.sdlc/and walk to the parent) — orstart_task.pyshould pass an explicit--project-rootderived from the worktree’s grandparent. → T-Y7QU-start-task-fails-loud-on-cache-missrun_quality_checks.py --diff-against-baselineexhibits the same pattern from a different angle: baseline lives under main’s.sdlc/quality-baselines/, executor looks under the worktree’s. Workaround: pass--baseline-dir <main-repo>/.sdlc/quality-baselinesexplicitly. Already filed as2026-05-25-task-work-step7-explicit-baseline-dir. → T-5X6Y-task-work-step7-explicit-baseline-dir/sdlc:task-workStep 2’s PR blocker (gh pr list --search "<basename>") treats a spec-only PR as if it were an implementation PR. We hit this on Task A and had to implement manually outside the protocol. The block prevents legitimate parallelism between “spec is up for review” and “implementer is racing ahead.” Worth a follow-up to distinguish the two PR shapes. →2026-05-27-task-work-step2-distinguishes-spec-pr-from-impl
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-Y7QU-start-task-fails-loud-on-cache-miss — linked: worktree-vs-main
_find_project_rootdivergence is the root cause; existing task’s AC-3 already targets it. - T-5X6Y-task-work-step7-explicit-baseline-dir — linked: same worktree-vs-main
.sdlc/resolution pattern, already filed. 2026-05-27-task-work-step2-distinguishes-spec-pr-from-impl— created: classify Step-2 candidate PRs as spec-only vs implementation; only the latter blocks pickup.
Depends on
Section titled “Depends on”T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics