Skip to content

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.md is 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 via plugin/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.py accepts --project-root and writes to <project-root>/docs/planning/tasks/. There’s no --upstream-repo notion, 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 with plugin/.claude-plugin/plugin.json. From a downstream consumer’s worktree, we can already discover the plugin’s repo path (and from there its origin URL via git config --get remote.origin.url).
  • We have gh available, and the user-allowlist includes broad Bash(gh pr *) patterns.
  • Entity types in plugin/entities/ today: backlog, epic, milestone, task. There is no request / bug-report / issue entity for cross-project communication.

A post-mortem follow-up flow that classifies each gap as one of:

  1. Local — the gap is about THIS project’s code/docs/planning. Spawn a task in the current repo, same as today.
  2. 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.
  3. 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 from task, 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.py in current repo, commit on feat branch).
  • For Upstream-plugin: clone-or-reuse the plugin repo’s worktree, create a feat/<gap-slug> branch from origin/main, run new_task.py against 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 frontmatter tags: array gets sdlc-meta appended when the upstream is the SDLC plugin (today the only one in play). This applies whether the dispatch lands cross-repo on the plugin’s main OR lands locally in the degenerate dev-repo == plugin-repo case — the tag travels with the task either way, so /sdlc:task-review and 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: request or bug-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.
  1. Entity bifurcation: reuse task. Cross-repo asks (both Upstream-plugin and Cross-project-request) use the existing task entity in the receiving repo’s backlog. The task shape is already right for the receiver. A distinct request / bug-report entity 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.
  2. Update plugin/entities/task/spawn-from-post-mortem.md with 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 includes tags: [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.
  3. Add a /sdlc:cross-repo-task-pr sub-skill (new) that takes {target-repo-path, slug, headline, body, branch-name} and: ensures a worktree exists for that repo, branches from its origin/main, runs new_task.py against the target’s project-root, commits, pushes, opens a PR via gh pr create --repo <owner/name>. Returns the PR URL.
  4. 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).
  5. Plugin-repo discovery: helper that resolves ~/.claude/plugins/<plugin-name> → repo root → origin URL. Live in plugin/scripts/ (e.g. resolve_plugin_repo.py).
  6. 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.
  7. 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’s main rather 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.
  • 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 the request / bug-report entity decision (likely).
  • AC-1: plugin/entities/task/spawn-from-post-mortem.md contains 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.md exists; invoking it with {target-repo-path, slug, headline, body, branch-name} produces a real PR against that repo’s origin/main and returns the URL on stdout. Verifiable via a fixture target-repo + gh pr view of the resulting PR.
  • AC-3: A /sdlc:task-work post-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 own docs/planning/tasks/. Agent-manual smoke test.
  • AC-4: plugin/skills/CLAUDE.md contains the convention statement.
  • AC-5: plugin/scripts/resolve_plugin_repo.py resolves 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 contains sdlc-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 by command grep of the spawned file’s frontmatter.
  • Implementing a new request or bug-report entity. 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.
  • none (depends on plugin/scripts/new_task.py and gh CLI, both already shipped/available)

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.

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

  • AC-1: auto — plugin/scripts/lint_skill_prose.py passes; the new step 2a in plugin/entities/task/spawn-from-post-mortem.md enumerates Local / Upstream-plugin / Cross-project-request and step 4 specifies per-classification dispatch.
  • AC-2: agent-manual — plugin/skills/cross-repo-task-pr/SKILL.md exists 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-work post-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.md now contains the convention statement “Post-mortem follow-ups about plugin code go upstream”; verifiable via command grep.
  • AC-5: auto — plugin/scripts/resolve_plugin_repo.py exists; ./plugin/scripts/resolve_plugin_repo.py sdlc returns /Users/sksizer/Developer/dev<TAB>git@github.com:sksizer/dev.git (exit 0); ./plugin/scripts/resolve_plugin_repo.py does-not-exist exits 1.
  • AC-6: agent-manual — scaffolding a task via new_task.py --tag sdlc-meta against a tmp project root produced a frontmatter tags: array containing sdlc-meta; confirmed via command grep of the spawned file. The spawn-from-post-mortem step 4 prose passes the tag through whether the dispatch is local or cross-repo.
  • start_task.py resumed 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 grep against plugin/entities/task/schema.json confirmed tags is free-form and <plugin-name>-meta values would not violate the schema — no schema-bump required.
  • 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 for source: directory marketplaces the path may point at a subdirectory of the real git repo. The resolver had to walk up to the git toplevel. The spec’s Today section should be updated next time a similar plugin-discovery spec is written, OR a /sdlc:task-ensure-ready check could grep the spec’s Today claims against the actual on-disk layout — but that’s open-ended. The narrower automation: ship a plugin/scripts/probe_plugin_install.py that prints whatever the plugin-discovery shape actually is on the current machine, so spec authors can paste accurate Today claims 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 from deferred-user to auto. → 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 in lint_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.py correctly 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 the READY: 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-work Step 10 / 5a; this run confirms the existing fix is doing its job.
  • T-MJGM-probe-plugin-install-shape — ship a diagnostic that prints the local Claude Code plugin-install layout so spec authors can paste accurate Today claims (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-user to auto (created).
  • 2026-05-21-lint-skill-prose-normalize-whitespace — collapse whitespace before phrase matching so line-wrapped required phrases match (created).

← Back to Tasks