/handover — External Repo Handover Assessment
SkillDev toolsOnboard an external repo via a structured handover assessment + harnessability scoring across 5 codebase dimensions.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /handover — External Repo Handover Assessment skill
What this skill tells your AI
The instructions your AI receives, as published by me2resh/apexyard in .claude/skills/handover/SKILL.md and read by ahel’s review.
Adopt an external repo into ApexYard management. The skill reads the target repo, synthesises a structured handover document, and tells you which ApexYard roles, workflows, and hooks should kick in.
This is the bridge between "we just inherited this codebase" and "this codebase is now governed by our normal SDLC".
LSP-aware (optional, recommended)
The handover deep-dive — reading the codebase to populate the assessment — performs semantic code navigation: finding definitions, walking references, tracing handlers across modules. When a Git URL is given, the skill clones the repo into workspace/<name>/ at step 1.5-clone (default, before any reads begin). With LSP enabled (ENABLE_LSP_TOOL=1 + per-language plugin per docs/getting-started.md) and the repo cloned locally, queries are ~3-15× cheaper in token cost than grep + Read on shallow lookups, and ~1.4-5× cheaper on multi-hop traces. Without LSP — or when only metadata is available — the skill falls back to grep + Read transparently. No new failure mode, just optional speed during the deep-dive phase.
Per-language LSP plugins live in Claude Code's marketplace. Install once; the skill detects the active language and dispatches automatically.
Path resolution
Read the registry path via portfolio_registry, the per-project docs dir via portfolio_projects_dir, and the ideas backlog via portfolio_ideas_backlog — all from .claude/hooks/_lib-portfolio-paths.sh. Source the helper at the top of any bash block that touches those paths:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
registry=$(portfolio_registry)
Defaults match today's single-fork layout (./apexyard.projects.yaml, ./projects, ./projects/ideas-backlog.md). Adopters in split-portfolio mode override the portfolio.{registry, projects_dir, ideas_backlog} keys in .claude/project-config.json. Don't hardcode literal apexyard.projects.yaml or projects/ paths in bash blocks — the helper resolves whichever mode the adopter is in. See docs/multi-project.md.
Write targets (see me2resh/apexyard#373 + #443): paths documented as projects/<name>/X in this skill are canonical adopter-facing forms — implement them in bash as "${projects_dir}/<name>/X". Never construct from "${PWD}/projects/...", "$(git rev-parse --show-toplevel)/projects/...", or a literal ./projects/... — those break in split-portfolio v2 mode where projects_dir resolves to a sibling repo.
REQUIRED per-block preamble (see #443): Claude executes each bash block as a separate shell invocation. The projects_dir assignment from the Path resolution section above does NOT carry into later blocks. Every bash block that writes to a projects/<name>/X path MUST start with this three-line preamble so it's self-contained:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
# ... now write to "${projects_dir}/<name>/X"
The Path resolution section's example sources the helper once for documentation purposes; it does not absolve later blocks from sourcing it themselves. Treat each bash fence as a fresh process.
Usage
/handover legacy-billing-api
/handover legacy-billing-api ../legacy-billing-api
/handover marketing-site https://github.com/some-org/marketing-site
/handover marketing-site --topology typescript-nextjs
/handover legacy-billing-api --all # non-interactive: generate the full default set
/handover legacy-billing-api --interactive # explicit opt-in to the checklist (same as default)
The --topology <name> flag pre-selects a topology bundle and skips the interactive pick in step 1.5. Available v1 topologies: typescript-nextjs, python-fastapi, go-data-pipeline. See topologies/README.md and AgDR-0048.
Document-set flags (--all vs --interactive) — default is the checklist
By default the skill presents a document selection checklist (step 5.6) after the assessment is computed, so the operator opts in to exactly the artefacts they want and picks the template for each template-backed doc. Two flags override the prompt:
| Flag | Behaviour |
|---|---|
--all | Non-interactive. Generate the full default set with no checklist and the conventional template for each template-backed doc — byte-for-byte the pre-checklist behaviour. Use this for scripted / unattended runs or when you just want everything. |
--interactive (default) | Present the checklist + per-doc template pick. Equivalent to passing no flag. The flag exists so the default is nameable in scripts and docs. |
Default is --interactive because a handover rarely needs every artefact, and the wrong template choice on a template-backed doc is annoying to undo by hand. --all is the explicit escape that preserves today's muscle memory — existing invocations that pass neither flag now see the checklist; pass --all to keep the old no-prompt flow. The handover assessment + harnessability score are always generated regardless of flag — they are the skill's core output, not optional artefacts.
Output location
The skill always writes the handover assessment; the rest of the artefacts are selected via the step 5.6 checklist (or generated in full with --all):
projects/<name>/handover-assessment.md ← always (re)written (assessment + harnessability)
projects/<name>/architecture/container.md ← if selected + missing — stub L2 C4 diagram (default-ticked)
projects/<name>/architecture/context.md ← if selected + missing — stub L1 C4 diagram
projects/<name>/architecture/sequence-<flow>.md ← if selected + a clear flow exists
Richer artefacts the operator can select but which are owned by dedicated skills — DFD (/dfd), Feature Inventory (/extract-features), user-journey HTML (/journey), Architecture Vision (/tech-vision) — are handed off rather than generated inline (see step 5.6).
One opt-in (default-OFF) artefact is written into the target repo instead of the ops fork — the in-repo operating manual:
<target repo>/AGENTS.md ← if selected (default-OFF) + not already present — concise agent operating manual, delivered via a branch + PR (step 8.5)
<target repo>/CLAUDE.md ← only if no CLAUDE.md exists — a one-line `@AGENTS.md` import shim
This is the single exception to "read-only against the target repo" (Rule 1) — it is opt-in, confirmed per run, and PR-delivered (never a direct commit). See step 8.5 and AgDR-0073.
The folder lives in the ops repo (your fork of apexyard), alongside the rest of projects/.
If projects/<name>/ doesn't exist, create it. Also seed a projects/<name>/README.md stub if missing — see projects/README.md for the convention.
The architecture stub is written once and never overwritten — it's a starting point, not a generated artefact. After the first handover, any edits the team makes to refine the diagram survive re-runs of the skill.
Process
0. Mark this session as bootstrap (REQUIRED)
/handover may run before any tracker tickets exist for the project being adopted, so the require-active-ticket.sh PreToolUse hook would block the registry / projects/<name>/ writes the skill needs. Write a marker so the hook exempts this skill (it's on the default bootstrap_skills list in .claude/project-config.defaults.json):
mkdir -p .claude/session && echo "handover" > .claude/session/active-bootstrap
Clear the marker on completion (Step "Post-Handover Checklist" below). If the skill is interrupted, the SessionStart hook clear-bootstrap-marker.sh clears it at the start of the next session. See AgDR-0011 + me2resh/apexyard#150.
Bootstrap scope — what IS and IS NOT exempt
The bootstrap exemption covers ONLY these writes:
apexyard.projects.yaml— registry append (step 7)projects/<name>/— assessment, architecture stubs (container / context / sequence), README (steps 5, 6, 6.1).claude/session/active-bootstrap— the marker itself (step 0)- Topology instantiation files (step 5.5, if a topology is picked)
It does NOT cover:
- Palette changes, UI work, or any other user request made during the session
- Creating or pushing new repositories
- Commits to branches without a ticket
If the user requests work outside the handover's scope mid-session, tell them: "That's outside the handover's bootstrap exemption — let me /start-ticket first." Then follow the normal SDLC: ticket → branch → PR → review.
1. Locate the target repo
If a path is given, use it. If a URL is given, proceed immediately to clone it into workspace/<name>/ (see step 1.5-clone below) — the user does not need to clone manually. If nothing is given, ask:
Where is the target repo? Local path or git URL?
1.5-clone. Clone the repo (URL path only — default yes)
When the operator provides a Git URL (step 1), clone it immediately before doing any further reads. Default is yes — no confirmation needed unless the operator explicitly passes --no-clone on the skill invocation. This is the cheapest moment: subsequent reads in steps 2–6 are 3–15× cheaper per query against a local clone than via the GitHub API.
Resolve the workspace dir and clone
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
WORKSPACE_DIR=$(portfolio_workspace_dir)
mkdir -p "$WORKSPACE_DIR"
if [ -d "$WORKSPACE_DIR/<name>/.git" ]; then
echo "✓ $WORKSPACE_DIR/<name>/ already exists — skipping clone."
else
git clone <repo-url> "$WORKSPACE_DIR/<name>"
fi
In single-fork mode WORKSPACE_DIR resolves to <ops-root>/workspace; in split-portfolio v2 mode it resolves to the sibling private repo (e.g. ../<fork>-portfolio/workspace). Don't hardcode workspace/<name>/.
On clone failure
If the clone fails (private repo without credentials, network error, repo moved): report the exit code, point at gh auth login or a manual git clone <repo-url> "$WORKSPACE_DIR/<name>" as the recovery, and continue with the local-path fallback. Do not retry or invent an alternative URL. The operator picks up from there.
If the operator explicitly passed --no-clone on the /handover invocation, skip this step silently and set $CLONE_STATUS="declined" for the step 10 summary.
Set the clone marker
CLONE_STATUS="cloned" # or "preserved" | "declined" | "failed: <reason>"
All subsequent reads in steps 2–6 use $WORKSPACE_DIR/<name>/ as the repo root whenever $CLONE_STATUS is cloned or preserved. When $CLONE_STATUS is declined or failed, fall back to GitHub API reads via gh api / gh -R <owner/name> … (degraded but functional).
bin/install-git-hooks.shis deliberately NOT invoked here. An earlier version of this step called it against the freshly-cloned workspace, on the reasoning that it mirrors/setup's call for the ops fork. It does not:/setuppointscore.hooksPathat the ops fork's own.githooks/, which ApexYard controls. Calling the same installer against an arbitrary, just-cloned third-party repo instead points git at that repo's own.githooks/(whatever it ships), and does so with no provenance check — the installer's only gate is "does a directory of that name exist." Git deliberately never clones$GIT_DIR/hooks, which is exactly what makesgit cloneof an untrusted repo a safe, read-only act; repointingcore.hooksPathat a tracked directory removes that property, and a hostile.githooks/post-checkoutor.githooks/reference-transactionthen runs as the operator on the very next ordinary git operation — no push required./handover's whole point is to evaluate a repo before deeper commitment, so this is exactly the wrong moment to hand it code execution. Confirmed by security review on PR #1087 (HIGH-1).The gap this leaves — a managed-project clone's own
.githooks/still isn't wired up by anything, so its terminalgit pushstays unprotected — is real and deliberately deferred, not silently dropped. The correct shape is for the framework to install its own hook into the clone's untracked.git/hooks/(never point at a tracked third-party directory), which needs its own design and its own ticket. Do not attempt it here.
1.5-reindex. Reindex the cloned repo in MCP (default: always attempt)
After a successful clone ($CLONE_STATUS=cloned), trigger an MCP reindex so search_code and search_docs return results during the deep-dive phases that follow (steps 2–6). Without this step those queries return empty against the just-cloned repo, and the agent silently falls back to find + cat + Bash — defeating the token-cost benefit of cloning early.
mcp__apexyard-search__reindex(scope="project", project="<name>")
On MCP unavailable: the call will error. Catch the error, print a single-line warning, set the marker, and continue. Do not skip silently — silent skips are indistinguishable between "server down" and "agent forgot the step", and the second failure mode is what this step exists to prevent.
⚠ MCP reindex unavailable — falling back to grep + Read for steps 2–6
REINDEX_STATUS="indexed" # or "unavailable" | "skipped" (when $CLONE_STATUS != cloned)
When $REINDEX_STATUS="indexed", prefer search_code and search_docs over grep + Read for the assessment reads in steps 2–6 (per the MCP-search-first rule). When unavailable or skipped, fall back to grep + Read without further apology.
A PostToolUse hook (suggest-mcp-reindex-after-clone.sh) fires after the clone command and emits a one-line reminder of this step. Same advisory shape as detect-role-trigger.sh — exit 0, non-blocking, removes the "I forgot the rule applied here" failure mode.
1.5. Pick a topology (default: skip / custom)
ApexYard ships harness-template topologies — bundles of curated handbooks + CI pipelines + AgDR templates per service shape. Picking one here pre-bakes the right governance surface for the stack; declining keeps the existing flow byte-for-byte. See topologies/README.md and AgDR-0048.
If the operator passed --topology <name> on the CLI, skip the interactive prompt and use that pick. Otherwise prompt:
Which topology fits this project?
[1] typescript-nextjs — TypeScript + Next.js web app (App Router, Prisma, JWT)
[2] python-fastapi — Python + FastAPI service (Pydantic v2, SQLAlchemy async, JWT)
[3] go-data-pipeline — Go batch / streaming pipeline (no HTTP surface)
[4] Skip / custom — no topology bundle; use the framework defaults
Read topologies/<name>/README.md for what each bundle includes.
[1/2/3/4 — default 4]
Branching:
- Pick 1/2/3: record the topology name in
$PICKED_TOPOLOGY(e.g.typescript-nextjs). Verify the topology dir exists at<ops_root>/topologies/<name>/. If missing (e.g. operator on an older framework version), print⚠ topology dir not found — falling back to no bundleand continue with$PICKED_TOPOLOGY="". - Pick 4 / default / any other input: set
$PICKED_TOPOLOGY="". Continue exactly as the pre-topology flow.
Verifying the pick. Read the topology's README.md and VERSION files. Print a one-line confirmation:
Topology: typescript-nextjs v1.0.0 — will instantiate 11 files into projects/<name>/ and workspace/<name>/.github/workflows/. (See step 5.5.)
If $PICKED_TOPOLOGY="", print nothing — the rest of the flow is unchanged.
1.6. Harness selection (skippable)
Background. As of 2026-07-09 the framework's mechanical gates reach beyond Claude Code through thin per-harness adapters — see docs/harnesses/README.md, the single source of truth for the support matrix. A team adopting an existing repo may already be running (or want to run) a non-Claude-Code harness against it; this step surfaces the adapter path at handover time instead of leaving the adopter to discover docs/harnesses/ on their own. Light and skippable — Claude Code adopters (the default) answer one question and move on.
Ask:
Which harness(es) will drive work on {name}?
[1] Claude Code (default — the native, full experience)
[2] opencode
[3] pi (pi.dev)
[4] Codex
[5] Cursor
[6] Other / not sure
[1-6, comma-separated for more than one — default 1]
Branch on the answer:
-
1 / default / empty → continue straight to step 2. No further output.
-
6 → one line — "No adapter for that harness yet. The mechanical gates (
.claude/hooks/*.sh) are portable bash; seedocs/harnesses/README.md§ 'Adapter-authoring pattern for future harnesses' if you want to write one." — then continue to step 2. -
2 / 3 / 4 / 5 (one or more) → for each, print its install command + its one precondition, read fresh from
docs/harnesses/README.md(single source of truth — don't duplicate the matrix here as a maintained copy). As of the 2026-07-09 matrix:Harness Install Precondition Tier opencode bash bin/install-opencode-adapter.shrun opencode headless with --auto✅ live-proven pi bash bin/install-pi-adapter.shrun pi headless with -a/--approve✅ live-proven Codex bash bin/sync-codex-adapter.shgrant hook-trust — /hooksinteractively,--dangerously-bypass-hook-trustfor a one-off headless run, or a user-level~/.codex/hooks.json✅ live-proven Cursor bin/install-cursor-adapter.shinstalls to user-level ~/.cursor/hooks.json🟡 failClosed-only — not live-proven; the cursor-agentCLI ignores hooks entirelyHonesty is load-bearing — never round Cursor up. State the tier exactly as
docs/harnesses/README.mddoes: opencode/pi/Codex are live-proven (a real credentialed model turn was actually blocked by the delegated gate); Cursor only fails closed on a hook-runner error, a materially weaker guarantee than verified delegated execution. Link the per-harness page for the full workflow:docs/harnesses/<harness>.md.
Don't run the install command yourself — print it, don't execute bin/install-*-adapter.sh against the target repo on the operator's behalf. Adapter install is a decision for the team taking ownership, not a side effect of adoption.
Record the pick as $SELECTED_HARNESSES (comma-separated, or claude-code if 1/default) for the step 10 summary.
1.7. Check server-side branch protection (advisory, default: always attempt)
Why this exists. AgDR-0115 records the decision that ApexYard does NOT install any client-side push protection into a managed-project clone — doing so would mean pointing
core.hooksPathat that clone's own tracked.githooks/, which is precisely the HIGH-risk wiring #1087's security review removed (see the "1.5-clone" note above andbin/install-git-hooks.sh's header). The accepted consequence: a human runninggit push origin mainfrom a terminal inside this clone is not blocked by anything ApexYard installs — only agent-driven pushes stay covered, via the ops-fork's ownPreToolUsehooks. For that residual, the managed repo's own server-side branch protection is the actual control, not a client-side backstop ApexYard ships. This step only checks whether that control is turned on and nudges the operator if it looks like it isn't — it is advisory, forge-agnostic, and fail-open: a missing check, a network failure, or an unsupported tracker kind never blocks the rest of/handover.
Only run this check when the target repo's <owner/name> is known (a Git URL was given in step 1). A plain local path with no resolvable remote host has nothing to query: set BRANCH_PROTECTION_STATUS="skipped (no remote host)", print nothing, and continue straight to step 2.
Otherwise, resolve the default branch (prefer the local clone when available; fall back to the API) and the tracker kind, dispatching the protection check the same way tracker_review_submit / tracker_pr_merge already dispatch on tracker_kind (.claude/hooks/_lib-tracker.sh) rather than hardcoding one forge's CLI:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-tracker.sh"
kind=$(tracker_kind "<owner/name>")
default_branch=""
if [ "$CLONE_STATUS" = "cloned" ] || [ "$CLONE_STATUS" = "preserved" ]; then
default_branch=$(git -C "$WORKSPACE_DIR/<name>" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')
fi
if [ -z "$default_branch" ] && [ "$kind" = "gh" ]; then
default_branch=$(gh api "repos/<owner/name>" --jq .default_branch 2>/dev/null)
fi
BRANCH_PROTECTION_STATUS="skipped (default branch unknown)"
if [ -n "$default_branch" ]; then
case "$kind" in
gh)
# gh api exits non-zero on BOTH "404 not protected" and a real
# failure (auth/network/no-permission/rate-limit). This check does
# not need to tell those apart — the response is the same either
# way: advise, never block. Fail-open by construction, not by
# accident.
if gh api "repos/<owner/name>/branches/${default_branch}/protection" >/dev/null 2>&1; then
BRANCH_PROTECTION_STATUS="protected"
else
BRANCH_PROTECTION_STATUS="not protected (or could not be verified)"
fi
;;
glab)
encoded=$(printf '%s' "<owner/name>" | sed 's#/#%2F#')
if glab api "projects/${encoded}/protected_branches/${default_branch}" >/dev/null 2>&1; then
BRANCH_PROTECTION_STATUS="protected"
else
BRANCH_PROTECTION_STATUS="not protected (or could not be verified)"
fi
;;
none|custom|*)
# No queryable, generically-shaped REST endpoint for these kinds —
# a `custom` tracker's PR/MR host is unknown, and `none` has no host
# at all. Say so plainly rather than guessing at an API shape.
BRANCH_PROTECTION_STATUS="skipped (no queryable forge for tracker.kind=${kind} — check your forge's branch protection settings manually)"
;;
esac
fi
If $BRANCH_PROTECTION_STATUS is "not protected (or could not be verified)", print this nudge once (non-blocking — continue straight to step 2 regardless):
⚠ <name>'s default branch (<default_branch>) has no server-side branch
protection turned on (or it couldn't be confirmed). ApexYard's own hooks
only cover agent-driven pushes in this clone — a human running `git push`
from a terminal here is not blocked. Server-side branch protection on the
forge is the actual control for that gap (see AgDR-0115). Recommended:
require a pull request before merging to <default_branch>.
If $BRANCH_PROTECTION_STATUS is "protected" or either "skipped" variant, print nothing further — the step 10 summary line is enough.
2. Read the surface area
Use $WORKSPACE_DIR/<name>/ as <repo> when available (clone succeeded or path was given). Fall back to GitHub API reads only when $CLONE_STATUS is declined or failed. Local reads are preferred — they are cheaper and more complete.
Without running anything destructive, gather:
# Tree (top 2 levels, prune node_modules / .git)
find <repo> -maxdepth 2 -type d \
! -path '*/node_modules*' \
! -path '*/.git*' \
! -path '*/dist*' \
! -path '*/build*'
# Key files
ls <repo>/README* <repo>/package.json <repo>/pyproject.toml \
<repo>/Cargo.toml <repo>/go.mod <repo>/Gemfile 2>/dev/null
# CI config
ls <repo>/.github/workflows/ 2>/dev/null
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 498
- Forks
- 271
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
handover-me2resh- Source
- github.com/me2resh/apexyard