T-KD48-post-mortem-routes-cross-repo-tasks
Status: closed/done · Impact: high · Complexity: medium
When a /sdlc:task-work post-mortem (Step 8) surfaces a gap that belongs to a different repo
than the one the task is running in — typically the sdlc plugin itself, or another upstream library
a consumer project depends on — the spawned follow-up task currently lands in the current repo’s
docs/planning/tasks/. That’s wrong: it accumulates plugin-bugs inside every consumer project,
fragments the upstream backlog, and silently duplicates work across repos. This task is to bifurcate
the spawn flow so cross-repo gaps route to the right upstream repo (as a PR against that repo’s
planning tree), while genuinely local gaps continue to land in the current repo as today.
plugin/entities/task/spawn-from-post-mortem.mdis the procedure consumed by the Step 8 sub-agent in/sdlc:task-work. It walks the “Friction and automation gaps” bullets and creates one task file per gap viaplugin/scripts/new_task.py, all relative to the current--project-root. There is no concept of “this gap belongs to another repo.”- Every post-mortem in this session demonstrates the problem: the orchestrate, pr-check,
task-close-out, task-work, and astro-docs post-mortems have spawned ~15+ follow-up tasks,
virtually all of which describe sdlc plugin gaps (orchestrator skill needs X, task-work skill
needs Y, pr-check skill needs Z). They live in
docs/planning/tasks/of this repo — which happens to also be the sdlc plugin’s own dev repo, so for THIS project the bifurcation is a degenerate case (current == upstream). For downstream consumers (a Tauri app, a separate research repo, etc.), the same flow would file plugin bugs inside their unrelated codebase. plugin/scripts/new_task.pyaccepts--project-rootand writes to<project-root>/docs/planning/tasks/. There’s no--upstream-reponotion, no PR-creation step, no remote dispatch.- The plugin’s identity is established by the
~/.claude/plugins/<name>symlink resolving to a checked-out repo withplugin/.claude-plugin/plugin.json. From a downstream consumer’s worktree, we can already discover the plugin’s repo path (and from there itsoriginURL viagit config --get remote.origin.url). - We have
ghavailable, and the user-allowlist includes broadBash(gh pr *)patterns. - Entity types in
plugin/entities/today:backlog,epic,milestone,task. There is norequest/bug-report/issueentity for cross-project communication.
Proposed
Section titled “Proposed”A post-mortem follow-up flow that classifies each gap as one of:
- Local — the gap is about THIS project’s code/docs/planning. Spawn a task in the current repo, same as today.
- Upstream-plugin — the gap is about a Claude Code plugin this project uses (most commonly the sdlc plugin itself). Spawn a task in the plugin’s repo via a PR against that repo, not in the current repo.
- Cross-project-request — the gap is a request from this project to a different
downstream/sibling project (“the data-model package needs to export type X for us”). This may
warrant a new entity type (
request/bug-report) distinct fromtask, since the receiving project owns scheduling.
Concretely after this lands:
- The spawn-from-post-mortem sub-agent first classifies each gap bullet with one of those three labels (with an AskUserQuestion fallback if classification confidence is low).
- For Local: same behavior as today (
new_task.pyin current repo, commit on feat branch). - For Upstream-plugin: clone-or-reuse the plugin repo’s worktree, create a
feat/<gap-slug>branch fromorigin/main, runnew_task.pyagainst the plugin repo, push, open a PR — all inside a dedicated sub-skill (e.g./sdlc:cross-repo-task-pr). Additionally, the spawned task’s frontmattertags:array getssdlc-metaappended when the upstream is the SDLC plugin (today the only one in play). This applies whether the dispatch lands cross-repo on the plugin’smainOR lands locally in the degenerate dev-repo == plugin-repo case — the tag travels with the task either way, so/sdlc:task-reviewand other downstream readers can see at-a-glance how much of the corpus is meta vs. project-domain work. - For Cross-project-request: same shape as Upstream-plugin but writes a different entity (TBD:
requestorbug-report) into the receiving project. Out of scope to implement fully in this task — the bifurcation surfaces it as a category and stubs a follow-up.
Approach
Section titled “Approach”- Entity bifurcation: reuse
task. Cross-repo asks (both Upstream-plugin and Cross-project-request) use the existingtaskentity in the receiving repo’s backlog. The task shape is already right for the receiver. A distinctrequest/bug-reportentity is out of scope for this PR; introduce one only if real friction emerges where the receiver wants distinct semantics (SLA, requester-attribution, blocking-relationship-to-our-work). When that happens, file a follow-up task that proposes the new entity. - Update
plugin/entities/task/spawn-from-post-mortem.mdwith a classification step. The sub-agent reads each gap bullet, decides Local / Upstream-plugin / Cross-project-request, and emits a structured per-bullet block (slug, classification, target-repo if non-local, headline, brief). When the classification is Upstream-plugin AND the resolved upstream is the SDLC plugin, the brief includestags: [sdlc-meta]so the spawned task (whether dispatched cross-repo or landing locally) carries the meta marker. Generalize the tag-emission rule in the prose:tags: [<plugin-name>-meta]for any future upstream, so the convention scales beyond SDLC if more plugins enter the picture. - Add a
/sdlc:cross-repo-task-prsub-skill (new) that takes{target-repo-path, slug, headline, body, branch-name}and: ensures a worktree exists for that repo, branches from itsorigin/main, runsnew_task.pyagainst the target’s project-root, commits, pushes, opens a PR viagh pr create --repo <owner/name>. Returns the PR URL. - Teach the spawn sub-agent to dispatch. For each classified bullet: Local → existing path; Upstream-plugin → call the new sub-skill with the plugin’s resolved repo path; Cross-project-request → call the new sub-skill, but flag in the brief that this is a request (not a self-claimed task).
- Plugin-repo discovery: helper that resolves
~/.claude/plugins/<plugin-name>→ repo root → origin URL. Live inplugin/scripts/(e.g.resolve_plugin_repo.py). - Convention update in
plugin/skills/CLAUDE.md: document that follow-ups about plugin behavior MUST PR against the plugin’s repo, not the consuming project. The new spawn flow enforces this automatically, but the prose is the contract for future skill authors. - Backfill audit: identify which of the existing post-mortem-spawned tasks in
docs/planning/tasks/are actually sdlc-plugin gaps that — were this task in place when they were spawned — would have lived on the plugin repo’smainrather than as drafts here. For this project the answer is “all of them, because dev repo == plugin repo” so the audit is a no-op here, but document the audit procedure so downstream consumers can run it.
Files to touch
Section titled “Files to touch”plugin/entities/task/spawn-from-post-mortem.md— add classification step + per-classification dispatch.plugin/skills/cross-repo-task-pr/SKILL.md(new)— the dispatch sub-skill.plugin/skills/cross-repo-task-pr/invariants.yaml(new)— required phrases, forbidden phrases, return markers.plugin/scripts/resolve_plugin_repo.py(new)— resolves a plugin name to its origin repo path + URL.plugin/skills/CLAUDE.md— convention: post-mortem follow-ups about plugin code go upstream.docs/planning/tasks/<spawned-followup>— if this task itself spawns a follow-up for therequest/bug-reportentity decision (likely).
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/entities/task/spawn-from-post-mortem.mdcontains an explicit classification step that enumerates Local / Upstream-plugin / Cross-project-request and specifies the dispatch for each. - AC-2:
plugin/skills/cross-repo-task-pr/SKILL.mdexists; invoking it with{target-repo-path, slug, headline, body, branch-name}produces a real PR against that repo’sorigin/mainand returns the URL on stdout. Verifiable via a fixture target-repo +gh pr viewof the resulting PR. - AC-3: A
/sdlc:task-workpost-mortem in a downstream consumer project (test fixture: a scratch repo that consumes the sdlc plugin) routes plugin-gap follow-ups to a PR against the sdlc plugin’s repo and routes local-gap follow-ups to the consumer’s owndocs/planning/tasks/. Agent-manual smoke test. - AC-4:
plugin/skills/CLAUDE.mdcontains the convention statement. - AC-5:
plugin/scripts/resolve_plugin_repo.pyresolves a plugin name (e.g.sdlc) to(repo-root-path, origin-url)via the~/.claude/plugins/<name>symlink, exits non-zero if the plugin isn’t installed. - AC-6: Spawn-from-post-mortem run against a fixture friction bullet that classifies as
Upstream-plugin (sdlc) emits a task whose frontmatter
tags:array containssdlc-meta. The tag lands on the spawned task whether the dispatch routes cross-repo (PR against the plugin’s main) or falls into the degenerate-local case (dev-repo == plugin-repo). Verifiable bycommand grepof the spawned file’s frontmatter.
Out of scope
Section titled “Out of scope”- Implementing a new
requestorbug-reportentity. Approach step 1 punts that decision; this task only puts the classifier and dispatcher in place. The entity-shape question is a follow-up. - Multi-hop dispatch (project A asks B to ask C). The dispatch is single-target: classify → file PR on one named repo. Daisy-chained requests are a different problem.
- Auto-merging the resulting PRs. Cross-repo PRs always go through normal review on the receiving repo.
- A web UI / dashboard for tracking cross-repo requests. The PRs themselves are the surface.
- Backfilling existing follow-up tasks already in
docs/planning/tasks/to “move” them to the plugin repo. For this project that’s a no-op (dev repo == plugin repo); for actual downstream consumers, the backfill is their own task to run after adopting this flow.
Dependencies
Section titled “Dependencies”- none (depends on
plugin/scripts/new_task.pyandghCLI, both already shipped/available)
Discovery context
Section titled “Discovery context”Surfaced 2026-05-20 by the user mid-session during a /sdlc:task-review run. Quote: “during the
post-mortem phase when downstream clients identify problems with other repos — and notably with the
sdlc plugin itself it needs to make PRs against the SDLC repo with those tasks, rather than putting
them in their own repo. These are dev repo tasks not downstream consumer issues.” Flagged as
high impact by the user because the project is “building up duplicate tasks that really belong
against the dev repo.” Naming: the user explicitly raised the possibility that cross-project asks
deserve a distinct entity (request, bug report) rather than reusing task.
This session itself is the test case: every post-mortem-spawned task from this session’s runs (orchestrate, pr-check, task-close-out, task-work, astro-docs) is about the sdlc plugin. The degenerate happy-coincidence that we’re working IN the plugin’s repo is the only reason they aren’t visibly mislocated. Any downstream consumer running this same flow today would be silently misfiling them.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
plugin/scripts/lint_skill_prose.pypasses; the new step 2a inplugin/entities/task/spawn-from-post-mortem.mdenumerates Local / Upstream-plugin / Cross-project-request and step 4 specifies per-classification dispatch. - AC-2: agent-manual —
plugin/skills/cross-repo-task-pr/SKILL.mdexists with deterministic markers (DONE / REHEARSED / EXISTING-PR / ERROR); the prose linter pins them. End-to-end PR creation against a fixture repo is deferred to a downstream consumer test (no fixture target repo available in this dev repo); the skill contract is fully specified. - AC-3: deferred-user — a real downstream-consumer smoke test requires spinning up a fixture repo
that consumes the sdlc plugin and running a
/sdlc:task-workpost-mortem through it. The skill prose specifies the behavior; cross-validation in a true consumer project is the user’s next step. - AC-4: auto —
plugin/skills/CLAUDE.mdnow contains the convention statement “Post-mortem follow-ups about plugin code go upstream”; verifiable viacommand grep. - AC-5: auto —
plugin/scripts/resolve_plugin_repo.pyexists;./plugin/scripts/resolve_plugin_repo.py sdlcreturns/Users/sksizer/Developer/dev<TAB>git@github.com:sksizer/dev.git(exit 0);./plugin/scripts/resolve_plugin_repo.py does-not-existexits 1. - AC-6: agent-manual — scaffolding a task via
new_task.py --tag sdlc-metaagainst a tmp project root produced a frontmattertags:array containingsdlc-meta; confirmed viacommand grepof the spawned file. The spawn-from-post-mortem step 4 prose passes the tag through whether the dispatch is local or cross-repo.
What worked
Section titled “What worked”start_task.pyresumed cleanly from the prior bail at ensure-ready; one invocation landed the chore-start commit on main and rebased the feat branch in a single shot.- The skill-prose linter caught a wrap-breakage in an invariant phrase before the commit landed, which is exactly the kind of drift the invariants.yaml shape is designed to catch.
command grepagainstplugin/entities/task/schema.jsonconfirmedtagsis free-form and<plugin-name>-metavalues would not violate the schema — no schema-bump required.
Friction and automation gaps
Section titled “Friction and automation gaps”- The spec assumed
~/.claude/plugins/<plugin-name>was a symlink to the plugin’s checked-out repo. Reality is two JSON files (installed_plugins.json+known_marketplaces.json), and forsource: directorymarketplaces thepathmay point at a subdirectory of the real git repo. The resolver had to walk up to the git toplevel. The spec’sTodaysection should be updated next time a similar plugin-discovery spec is written, OR a/sdlc:task-ensure-readycheck could grep the spec’sTodayclaims against the actual on-disk layout — but that’s open-ended. The narrower automation: ship aplugin/scripts/probe_plugin_install.pythat prints whatever the plugin-discovery shape actually is on the current machine, so spec authors can paste accurateTodayclaims instead of guessing from old memory. → T-MJGM-probe-plugin-install-shape - A real downstream-consumer smoke test (AC-3) requires a fixture consumer repo. The post-mortem
flow doesn’t currently scaffold one for cross-repo validation. A
plugin/skills/cross-repo-task-pr/tests/directory with a fixture target repo + an eval harness invocation would let AC-3 graduate fromdeferred-usertoauto. → T-KECI-cross-repo-task-pr-fixture-tests - The skill-prose linter’s
section: "2."match is fragile against line-wrapped phrases — a phrase like"never in its main checkout"failed because the prose wrapped between “never” and “in”. The linter could either (a) normalize whitespace before matching, or (b) document the constraint inlint_skill_prose.py --help. Today the author has to discover the issue by running the linter and reading the violation. A doc note in the linter help is the cheap fix; whitespace normalization is the right one. →2026-05-21-lint-skill-prose-normalize-whitespace start_task.pycorrectly handled the resumption case (the prior run had landed the ensure-ready stamp commit but not the start-commit), but the failure-mode handoff from the previous task-work invocation (which bailed at theREADY:marker) was only recoverable because the parent orchestrator passed explicit re-dispatch context. The task-work skill’s terminal-marker contract is already tightened in/sdlc:task-workStep 10 / 5a; this run confirms the existing fix is doing its job.
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-MJGM-probe-plugin-install-shape — ship a diagnostic that prints the local Claude Code
plugin-install layout so spec authors can paste accurate
Todayclaims (created). - T-KECI-cross-repo-task-pr-fixture-tests — add a fixture consumer repo + eval harness so AC-3
of this task can graduate from
deferred-usertoauto(created). 2026-05-21-lint-skill-prose-normalize-whitespace— collapse whitespace before phrase matching so line-wrapped required phrases match (created).