On confirmation, execute these steps in order:
a. Preserve handoff.md — do not delete it. It is the last live session state. It is bannered as an archive marker at its new location (step 6d1) and carried into completed-tp-designs/ by the git mv in step 6c, so the local archive retains the real session prose for /tp-session-restore on this machine. handoff.md is gitignored by design (skills/tp-session-save/SKILL.md — session state may reference sensitive configuration, so it is deliberately kept out of version control): it therefore stays a local-only file and is not committed to the completion PR. "Archive" here means preserve-and-banner in place, not commit to VCS — the point is to stop deleting it, so the local operator can still restore the session, without moving session context into version control.
b. Create three-pillars-docs/completed-tp-designs/ directory if it doesn't exist.
b1. Merge any spec delta before archiving: If three-pillars-docs/tp-designs/{design-name}/spec-delta.md exists, run /tp-spec merge {design-name} to merge it into the domain base spec at three-pillars-docs/specs/<domain>/spec.md. This step is a no-op skip when the design has no spec-delta.md (e.g., designs not scaffolded via /tp-spec add, like living-spec-layer itself) — if three-pillars-docs/tp-designs/{design-name}/spec-delta.md does not exist, skip the merge sub-step (no-op) and proceed to the git mv. If the merge is blocked (conflict or parse error), surface the error and stop — do not proceed with archival until the delta is clean or deliberately removed.
c. Move the design directory using git mv three-pillars-docs/tp-designs/{design-name} three-pillars-docs/completed-tp-designs/{design-name} to preserve git history.
d. Stamp the completion date on design.md at its new location. Edit three-pillars-docs/completed-tp-designs/{design-name}/design.md. If it already has YAML frontmatter (starts with ---), add a completed: YYYY-MM-DD field to the existing frontmatter. If it has no frontmatter, prepend a new frontmatter block:
---
completed: YYYY-MM-DD
---
followed by the existing content (preserve a blank line between the closing --- and the document body).
Why edit after the move, not before: git mv transfers the index entry, not the working-tree content. If you edit design.md before the move, that edit becomes an unstaged modification at the old path; git mv then stages a pure rename with the pre-edit content, and your frontmatter quietly fails to make it into the archival commit. Editing at the new location avoids the trap — step 6f's git add of the new design.md path catches the rename and the modification in one shot.
d1. Banner the archived handoff.md at its new location (step 6d1). If three-pillars-docs/completed-tp-designs/{design-name}/handoff.md does not exist (a pre-rule design, or a design that never wrote one), skip this sub-step cleanly — there is nothing to banner. Otherwise, prepend the dual archive marker to it: a machine frontmatter flag and a human blockquote.
- If it already has YAML frontmatter, add
archived: true and completed: YYYY-MM-DD fields to it. If it has none, prepend a new frontmatter block:
---
archived: true
completed: YYYY-MM-DD
---
- Immediately after the frontmatter, insert the human-readable blockquote:
> **📦 Archived handoff** — `{design-name}` was completed YYYY-MM-DD and archived to `completed-tp-designs/`. This is the last live session state, preserved for reference; it is not an active handoff.
followed by the existing handoff content.
This is a local-only edit — do NOT git add it. handoff.md is gitignored (.gitignore ignores tp-designs/*/handoff.md and completed-tp-designs/*/handoff.md; session state stays out of VCS per skills/tp-session-save/SKILL.md). The banner is applied to the moved file in the working tree and is not staged or committed — git add would refuse the ignored path anyway (and forcing it with -f would defeat the security policy). The bannered handoff is the local operator's record, read by /tp-session-restore on this machine; it never enters the completion PR. Edit at the NEW path (completed-tp-designs/{design-name}/handoff.md) so the local file that /tp-session-restore will resolve carries the banner.
e. Update Current Focus in product_roadmap.md: If the roadmap has a ## Current Focus table containing this design, remove its row and shift remaining priorities up (Priority 2 → 1, etc.). If removing this row unblocks another row (was listed in its "Blocked By"), clear the blocker and update its "Next Action" to the now-available step. Show the proposed changes and get user confirmation before writing.
f. Commit the archival changes on the current branch:
- First, set
phase = "cleanup-pending" on lock.json at its archived location (three-pillars-docs/completed-tp-designs/{design-name}/lock.json — the git mv in step 6c moved it). This marker MUST land in the archival commit below, not in a later staging step, or it never reaches the PR and /tp-post-merge's discovery gate can't find the design. (Step 6g references this as already-committed.)
- Run archive-time cite rewrite (before staging), capturing the JSON:
RECONCILE_JSON="$(python3 "$TP_ROOT"/skills/_shared/reconcile_docs.py --archive-cites --slug {design-name} --apply --json)". This rewrites any tp-designs/{design-name} path cites in code and living docs to completed-tp-designs/{design-name} so cites on {base} are never dead between archive and post-merge. The --json output lists the rewritten file paths as an edits[] array (each entry carries a path). Stage exactly the --json file list alongside the archival paths — by looping over it (the staging block below), never a fixed hardcoded set, so a rewrite outside the fixed archival paths is never left unstaged. --auto runs this sub-step identically (mechanical, no prompt).
- Run
git status --short and verify the only changes are the archival paths + the reconcile --json file list: the old three-pillars-docs/tp-designs/{design-name}/ (as deletions from the rename), the new three-pillars-docs/completed-tp-designs/{design-name}/ (additions, including any previously-untracked files like demos and decisions.md from a pre-rule-change design), optionally three-pillars-docs/product_roadmap.md (modified), and the rewritten files from the --json list. If unrelated changes appear in the working tree, stop and tell the user to commit or stash them first — the skill won't sweep unrelated WIP into the completion commit.
- Stage the archival paths explicitly (do NOT use
git add -A or git add .). The git mv from step 6c only stages tracked files; any untracked siblings in the design directory (e.g., decisions.md or demos/ content from a pre-2026-05 design before those became tracked-from-creation) need explicit re-add at the new path. The git add of the new design.md path is also what captures the frontmatter stamp from step 6d — git mv staged a pure rename, this add rewrites the index entry with the post-edit content:
git add three-pillars-docs/tp-designs/{design-name} three-pillars-docs/completed-tp-designs/{design-name}/design.md
git add three-pillars-docs/completed-tp-designs/{design-name}/decisions.md 2>/dev/null
git add three-pillars-docs/completed-tp-designs/{design-name}/demos/ 2>/dev/null
git add three-pillars-docs/product_roadmap.md
git add three-pillars-docs/completed-tp-designs/{design-name}/lock.json
git add three-pillars-docs/specs/<domain>/spec.md 2>/dev/null || true
for f in $(printf '%s' "$RECONCILE_JSON" | python3 -c 'import sys,json; [print(e["path"]) for e in json.load(sys.stdin).get("edits", [])]'); do
git add "$f"
done
git status --short
After this, git status should show no remaining untracked entries under three-pillars-docs/completed-tp-designs/{design-name}/. If any do, stage them too — the archive must be complete.
- Staged-blob guard — run AFTER staging, strictly BEFORE the commit. The archived
design.md/lock.json staged index blobs (not the working-tree files) must carry the completion stamp / cleanup-pending phase, and no live tp-designs/{design-name} cite may survive — inspecting the blob after the commit would be inert. Run the read-only helper:
python3 "$TP_ROOT"/skills/tp-design-complete/scripts/verify_archive_staged.py \
--repo "$(git rev-parse --show-toplevel)" --slug {design-name}
- Exit
0 → all staged-blob assertions pass; proceed to the commit.
- Exit
1 (an assertion failed — a stamp/phase/cite that reached only the working tree, not the index): re-git add the specific offending path named on the helper's stderr repair line (the double-add — the fix for a stampful working tree whose index blob is stale), then re-run the helper. If it still exits 1, abort loudly with the repair message and a non-zero exit — never commit a stampless / lock-phaseless archive.
- Exit
2 (a precondition error — an archived path absent from the index, a bad slug): abort IMMEDIATELY with a distinct precondition message and a non-zero exit. Do not run the double-add retry — re-adding a path that is not there cannot fix a precondition error.
- Under
--auto: on any abort (exit 1 still-failing, or exit 2), log the abort to three-pillars-docs/tp-designs/{design-name}/decisions.md (the sanctioned --auto audit-trail write, same class as the BLOCKED-learn logged exception) and exit non-zero so the orchestrator escalates.
- Commit with a focused message. The commit runs UNPIPED (no
| tail, no | head, no subshell that would swallow a hook's non-zero exit); capture the pre-commit HEAD first and, after the commit, assert HEAD advanced — a hook-blocked commit leaves HEAD unchanged and is a failure, not a success:
PRE=$(git rev-parse HEAD)
git commit -m "Complete design: {design-name}"
if [ "$(git rev-parse HEAD)" = "$PRE" ]; then
echo "archival commit did not advance HEAD (hook-blocked?) — aborting, not reporting success" >&2
exit 1
fi
Do not add any Co-Authored-By trailer — respect the user's commit-style preferences.
- If the commit fails (e.g., a pre-commit hook blocks it → HEAD unchanged per the check above), stop and surface the hook output to the user. Do not retry with
--no-verify, and do not proceed to the PR step.
g. Offer to open a PR back to the base branch:
- The
phase = "cleanup-pending" marker was set and committed as part of the archival commit in step 6f (do not re-stage it here). If you somehow reach this step with the lock still showing an earlier phase (e.g., 6f was hand-run without it), set it now and amend the archival commit (git add .../lock.json && git commit --amend --no-edit) before opening the PR — the marker must be on the branch the PR ships, or /tp-post-merge's discovery gate can't find the design.
This phase marker is the discovery signal that tells /tp-post-merge (and /tp-merge-from-main step 8) the design is awaiting teardown — it is not the safety gate. The actual gate is /tp-post-merge's own merge verification (verify_merged.py); /tp-post-merge is the sole skill that runs post-merge cleanup, and it tears down only on a verified merge regardless of this marker.
- Resolve the default branch: try
git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null and strip origin/; if that returns nothing, fall back to main (then master) if the remote has that ref (git ls-remote --heads origin main / master). Call this {default}.
- Parent-aware base resolution — the current branch may have been cut from another in-flight design's branch rather than from
{default}. The PR should target the parent in that case so the design's lineage is preserved through to merge.
- Compute the repo root:
ROOT="$(git rev-parse --show-toplevel)".
- Run
python3 "$TP_ROOT"/skills/tp-design-complete/scripts/detect_parent.py --repo "$ROOT" --design {design-name} --default-branch {default}. The script inspects sibling designs' lock.json files (no namespace pattern matching — any branch name declared in lock.json is honored) to find branches that are ancestors of HEAD and have not been merged into {default}. Capture its stdout as JSON.
- On exit 0, parse the JSON and branch on the
verdict field:
- none → set
{base} = {default}. No prompt.
- single → tell the user:
Detected parent design '{design}' on branch '{branch}'. Target this for the merge instead of '{default}'? (parent / default / other). On parent, set {base} = {branch}. On default, set {base} = {default}. On other, prompt for an explicit branch name and use that.
- multiple → list each candidate numbered with design / branch /
last_touched, plus default and other as keywords. Prompt for a number or keyword and set {base} accordingly.
- On non-zero exit or JSON parse failure, log a one-line warning (
parent-detection unavailable; defaulting to {default}) and set {base} = {default}. The default-branch path must always remain reachable — a buggy detector must never block a completion.
- Failure modes the heuristic can't catch (worth knowing for when to override): rebased branches lose their original creation point so the merge-base ancestry breaks; force-pushed parents shift the merge-base under the script's feet; and if the user fast-forward-merged the parent into a different sibling, both will look like ancestors of HEAD until the parent gets deleted. The interactive prompt gives the user the last word — fall back to
default or other when the detected target looks wrong.
- Get the current branch:
git branch --show-current.
- Skip the PR offer and tell the user how to push manually if any of these are true:
- No
origin remote (git remote get-url origin fails).
- Current branch equals the resolved base branch — there is nothing to PR; the archival commit is already on base and the user can push directly when ready.
- Otherwise, ask:
Push {current-branch} and open a PR into {base}? (yes / no)
- On yes:
- On no: leave the commit in place and remind the user how to push + open a PR when ready.
- Never merge the PR from within the skill — review happens on the PR, not inside the skill.
- This skill ends at PR-open — teardown is
/tp-post-merge's sole responsibility. After reporting the PR URL, tell the user:
After the PR is merged, run /tp-post-merge {design-name} to delete the branch and worktree. If you use /tp-merge-from-main to sync the base in, its step 8 will auto-chain /tp-post-merge automatically once the completion PR has landed.
h. Report success with:
- The archive location:
three-pillars-docs/completed-tp-designs/{design-name}/
- The commit SHA produced in step 6f (short form)
- The PR URL if one was opened, or the branch name the user still needs to push/PR manually
- Next step:
/tp-post-merge {design-name} after the PR is merged (or /tp-merge-from-main step 8 auto-chains it)