update
SkillDev toolsSync the ApexYard fork with upstream — preview, merge-or-rebase on a sync branch, walk per-version migrations.
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 update skill
What this skill tells your AI
The instructions your AI receives, as published by me2resh/apexyard in .claude/skills/update/SKILL.md and read by ahel’s review.
/update — Sync ApexYard Fork from Upstream
Single-command replacement for the manual "fetch → branch → merge → push → PR" dance that fork maintainers do to pull upstream apexyard changes into their ops fork.
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"
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.
Usage
/update # merge-based sync (default, safer)
/update --rebase # rebase local customisations on top of upstream
/update --dry-run # preview only, don't touch anything
/update --from-version v1.2.0 # override the version anchor (use when fork anchor is missing/wrong)
/update --skip-migrations # files-only sync; do NOT run the per-version migration chain
/update --skip-adapter-sync # do not refresh an already-installed Codex adapter
/update --from-dev # (hidden) pull from upstream/dev — pre-release; expect breakage
Options
| Flag | Effect |
|---|---|
--rebase | Rebase local commits onto upstream instead of merging. Cleaner linear history; rewrites local SHAs. |
--dry-run | Run the preview step only. Print the commit delta and exit; no fetch-after-preview, no branch creation, no merge. Does NOT execute migrations — only previews the planned chain. |
--from-version vN.N.N | Explicit version-anchor override. Use when .claude/framework-version is missing (legacy fork pre-v1.4.0) OR you've manually rolled back and the anchor is stale. The chain is built against this value instead of the file. Refuses if the value doesn't match vMAJOR.MINOR.PATCH. |
--skip-migrations | Sync the framework files but DO NOT run the per-version migration chain. Prints an advisory warning naming each skipped pair and reminding the operator that the migrations can be replayed later with bash .claude/migrations/<pair>.sh. The anchor file IS still advanced to the new release tag, so subsequent runs won't re-offer the same migrations. Use sparingly — the chain is the point. |
--skip-adapter-sync | Do not refresh an already-installed Codex adapter. Detection is installation-based, not harness-session-based: without this flag, /update reconciles only a manifest-backed or complete legacy ApexYard Codex adapter and leaves uninstalled forks untouched. Intended as a troubleshooting escape hatch. |
--from-dev | Hidden / opt-in. Sync from upstream/dev (pre-release work) instead of the latest upstream/main tag. Prints a ⚠ PRE-RELEASE SYNC banner BEFORE any fetch/state-mutation. Same sync-branch + conflict-resolution flow; branch is named chore/sync-upstream-dev (or chore/#<TICKET>-sync-upstream-dev if a tracking issue is supplied). Intended for the framework maintainer (testing pre-release work on another machine) and for adopters who explicitly want to validate an upcoming framework change. Not in the skill's description frontmatter on purpose — /help should not surface it, since the adopter contract is tagged releases (see AgDR-0007 release-cut model). Combinable with --rebase and --dry-run. When --from-dev is set, the migration chain is automatically skipped — pre-release work doesn't have a release tag to anchor against. |
Output
On success: one sync branch ready to push (e.g. chore/#N-sync-upstream-apexyard), with an auto-generated PR body listing the commits pulled in, plus the exact next commands to run.
On conflict: paused at the conflict point with per-file options (keep mine / accept upstream / open editor).
On up-to-date: one line after reconciling any detected Codex adapter. Git refs remain unchanged, but stale generated adapter files may be refreshed.
When NOT to use
- The clone has no
upstreamremote. The skill prints the exactgit remote add upstream …command and exits. - The working tree is dirty (uncommitted changes or unstaged files). The skill refuses — stash or commit first.
- The current branch is not the default (
main/master). The skill refuses —git checkout mainfirst. - You want to sync a specific feature branch from upstream. Out of scope — this skill is for default-branch fork sync only.
Process
Pre-step: Parse flags + print pre-release banner (when --from-dev)
Parse the invocation arguments first, BEFORE any fetch / branch / merge work:
FROM_DEV=0
DRY_RUN=0
REBASE=0
SKIP_MIGRATIONS=0
SKIP_ADAPTER_SYNC=0
FROM_VERSION_OVERRIDE=""
while [ "$#" -gt 0 ]; do
case "$1" in
--from-dev) FROM_DEV=1 ;;
--dry-run) DRY_RUN=1 ;;
--rebase) REBASE=1 ;;
--skip-migrations) SKIP_MIGRATIONS=1 ;;
--skip-adapter-sync) SKIP_ADAPTER_SYNC=1 ;;
--from-version) shift; FROM_VERSION_OVERRIDE="$1" ;;
--from-version=*) FROM_VERSION_OVERRIDE="${1#--from-version=}" ;;
esac
shift
done
# Resolve the upstream ref + sync-branch suffix once, at the top, so every
# downstream step references the same target.
if [ "$FROM_DEV" = "1" ]; then
UPSTREAM_REF=upstream/dev
BRANCH_SUFFIX=sync-upstream-dev
# Pre-release work has no release tag — chain walking is meaningless.
SKIP_MIGRATIONS=1
else
UPSTREAM_REF=upstream/main
BRANCH_SUFFIX=sync-upstream-apexyard
fi
# Validate --from-version shape early (semver-core only; pre-release suffix
# is not supported, in line with the chain helper's contract).
if [ -n "$FROM_VERSION_OVERRIDE" ]; then
if ! echo "$FROM_VERSION_OVERRIDE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "--from-version: expected vMAJOR.MINOR.PATCH (got '$FROM_VERSION_OVERRIDE')." >&2
exit 1
fi
fi
If FROM_DEV=1, print this banner BEFORE doing anything else (in particular, before any git fetch, branch-create, or merge — operator must see the warning before any state mutation):
⚠ PRE-RELEASE SYNC — pulling from upstream/dev
This is unreleased work; expect breakage.
Revert with: git reset --hard origin/main
For supported updates, use /update (no flag) to pull tagged releases.
The banner restates the deal every invocation — an operator who used --from-dev once should not be surprised the next time they run plain /update and find themselves on a different code path. The banner is load-bearing on purpose: dropping it would let pre-release breakage land silently.
0. Mark this session as bootstrap (REQUIRED)
/update edits framework-root files (resolving merge conflicts, updating CLAUDE.md imports, etc.) which the require-active-ticket.sh PreToolUse hook would otherwise block when the only "ticket" is the upstream-sync work itself. 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 "update" > .claude/session/active-bootstrap
Clear the marker on completion (last step of this skill). 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.
1. Pre-flight
Run these checks in order. On first failure, stop and explain.
# 1a. upstream remote exists
git remote | grep -qx upstream || {
ORIGIN=$(git remote get-url origin)
echo "No 'upstream' remote configured."
echo "Add it with:"
echo " git remote add upstream https://github.com/me2resh/apexyard.git"
echo "Then re-run /update."
exit 1
}
# 1b. working tree is clean
if [ -n "$(git status --porcelain)" ]; then
echo "Working tree is dirty. Commit or stash first, then re-run /update."
exit 1
fi
# 1c. on default branch
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null | sed 's|origin/||')
DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "Not on default branch ($DEFAULT_BRANCH). Currently on: $CURRENT_BRANCH"
echo "Run: git checkout $DEFAULT_BRANCH"
exit 1
fi
1d. Define installed-adapter reconciliation
Detection belongs to the generator, not to the current harness session. The
helper below refreshes only a manifest-backed ApexYard Codex adapter or the
complete pre-manifest shape (.agents/skills/, .codex/agents/, and
.codex/hooks.json). An uninstalled or partial adapter is a silent no-op.
reconcile_installed_codex_adapter() {
if [ "$SKIP_ADAPTER_SYNC" = "1" ]; then
echo "Codex adapter reconciliation skipped (--skip-adapter-sync)."
return 0
fi
if [ "$DRY_RUN" = "1" ]; then
echo "DRY-RUN: would reconcile an installed Codex adapter."
return 0
fi
local script="$(git rev-parse --show-toplevel)/bin/sync-codex-adapter.sh"
if [ ! -f "$script" ]; then
echo "Codex adapter reconciliation failed: missing $script" >&2
return 1
fi
bash "$script" --reconcile-installed
}
Unlike the SessionStart advisory nudge described below, an explicit /update
is strict: a detected adapter that cannot be generated and verified makes the
update fail instead of being reported as current.
2. Fetch both remotes
git fetch upstream --quiet brings down all upstream branches by default (including upstream/dev), so a single fetch covers both the default and the --from-dev target. No conditional fetch needed.
git fetch upstream --quiet
git fetch origin --quiet
Network failure: print a warning and exit. Don't try to "work from cache" — users should know they're seeing stale state.
3. Preview
Two signals matter here: a new upstream tag (the actionable one, meaning a real release is available), and upstream main commits since the fork's last sync (informational — may just be a docs typo).
When --from-dev is set, the comparison target is upstream/dev instead of upstream/main, and tag-based signals are skipped (dev is by definition pre-release; there is no tag to compare against). The preview reports the commit delta against upstream/dev and the operator decides whether to proceed.
AHEAD=$(git rev-list --count "$UPSTREAM_REF"..main)
BEHIND=$(git rev-list --count main.."$UPSTREAM_REF")
# Tag-based signal applies only to the tagged-release path.
if [ "$FROM_DEV" = "0" ]; then
UPSTREAM_TAG=$(git tag --list --sort=-v:refname --merged upstream/main | head -n 1)
LOCAL_TAG=$(git tag --list --sort=-v:refname --merged main | head -n 1)
fi
Then report. Examples:
Up-to-date (no tag drift, no commit drift):
reconcile_installed_codex_adapter || exit 1
rm -f .claude/session/active-bootstrap
echo "Fork is up to date with upstream/main. Nothing to sync."
Exit 0.
Any other preview path that exits 0 without creating a sync branch (for
example, the operator declines unreleased upstream/main commits) MUST call
reconcile_installed_codex_adapter before cleanup and exit. This closes the
stale-adapter bug even when there is no release work to merge. --dry-run
reaches the same helper but prints intent without mutating files.
No release drift, but main has moved (common, NOT actionable):
Fork is on upstream's latest release (v1.1.0) but upstream/main has 3 unreleased commits.
These are typically docs tweaks, CI fixes, or work-in-progress.
Sync anyway? [y/N]
Default answer is "no" — small main commits aren't worth syncing. Surface this without nagging; the user can still choose to pull in bleeding-edge.
Behind only — new release available (actionable, default):
New release available: v1.1.0 (you are on v1.0.0, 12 commits behind upstream/main).
Upstream commits to pull in:
c8c93bb fix: merge-gate hooks read PR HEAD via gh pr view (#57)
1299b59 fix(#47): catch gh api .../merge bypass (#54)
5f067b5 fix: reject closed issue refs (#53)
... (9 more)
Proceed with merge? [Y/n]
Default answer is "yes" in this mode — there's a real release the user asked about by running /update.
--from-dev (pre-release):
Pre-release sync: 7 unreleased commits on upstream/dev since fork's HEAD.
Upstream/dev commits to pull in:
ab12cde feat(#250): /update --from-dev hidden flag
cd34efg fix(#248): tighten validation
... (5 more)
Proceed with merge from upstream/dev? [Y/n]
Default answer is "yes" — the operator opted into pre-release explicitly with the flag, the banner already warned them about breakage, and asking again would be nagging. Skip the tag-based prompts entirely; dev has no tags to compare.
Ahead and behind (typical fork):
The prompt's default answer branches on whether a new release is available:
- If
UPSTREAM_TAGis strictly newer thanLOCAL_TAG→ default[Y/n](there's a real release to pull in). - If they're equal (no new release, just main drift) → default
[y/N](likely noise).
Fork has 5 local commits not in upstream, and is 12 commits behind.
Local commits (will be preserved on top of the merge):
f46d4e7 Merge pull request #2 from …/chore/#40-configure-ops-repo
840bb2d fix: auto-fix markdown lint in handover assessments
(… 3 more …)
Upstream commits to pull in:
c8c93bb fix: merge-gate hooks read PR HEAD via gh pr view (#57)
(… 11 more …)
New release available: v1.1.0 (you are on v1.0.0).
Proceed with merge? [Y/n]
Cap each list at 20 entries with an (N more) marker.
If --dry-run is set, show the preview and exit without touching anything else.
4. Ask merge vs rebase (unless --rebase was passed)
If not already specified by flag:
Sync strategy:
(1) merge — creates a merge commit. Local history is preserved as-is. Safer for shared branches. DEFAULT.
(2) rebase — replays local commits on top of upstream. Cleaner linear history but rewrites local SHAs.
Choose [1]:
Default is merge. Record the choice.
5. Create a sync branch
Rationale for diverging from the #58 AC wording ("leaves updated local main"): apexyard's own block-main-push.sh hook blocks direct pushes to main and also blocks commits made while on main. A merge with conflicts requires a git commit to finalise, which would be blocked. A sync branch sidesteps both issues and is the same shape the project uses for all other changes.
# Find or create a tracking issue. If a recent "sync" issue is open, reuse its number.
# Otherwise prompt the user to create one (or offer to create it via `gh issue create`).
# $BRANCH_SUFFIX was set in the pre-step:
# sync-upstream-apexyard for upstream/main (default)
# sync-upstream-dev for upstream/dev (--from-dev)
if [ -n "$TICKET" ]; then
BRANCH="chore/#${TICKET}-${BRANCH_SUFFIX}"
else
BRANCH="chore/${BRANCH_SUFFIX}"
fi
git checkout -b "$BRANCH"
6. Do the sync
$UPSTREAM_REF was set in the pre-step (upstream/main by default, upstream/dev under --from-dev).
Merge path:
git merge "$UPSTREAM_REF" --no-edit
Rebase path:
git rebase "$UPSTREAM_REF"
Capture stdout/stderr for the conflict-detection step.
7. Handle conflicts (if any)
If merge/rebase reports conflicts, show the user one file at a time:
CONFLICT in .claude/rules/pr-workflow.md
Upstream changed: adds "### Both merge shapes are gated (#47)" section
Local changed: inserted custom header paragraph at the top
Options:
(1) Keep mine — git checkout --ours .claude/rules/pr-workflow.md
(2) Accept upstream — git checkout --theirs .claude/rules/pr-workflow.md
(3) Open in editor — pause skill, wait for user to resolve, then resume
Choose [3]:
For each conflict file, get the user's choice. Default to (3) since auto-resolution on a governance framework is risky.
After each file: git add <file> to mark resolved.
When all conflicts are resolved:
# merge path
git commit --no-edit
# rebase path
git rebase --continue
If at any point the user wants to bail:
git merge --abort # or: git rebase --abort
git checkout main
git branch -D "$BRANCH"
8. Detect deprecated config keys (advisory)
After the merge / rebase has applied (so the new .claude/project-config.defaults.json is on disk), scan the adopter's .claude/project-config.json for top-level keys that no longer exist in defaults — typically a config block removed upstream (e.g. voice_prompts removed in me2resh/apexyard#157) that still lingers in the override as dead config.
This is advisory only. Custom-extension keys an adopter has added (their own hooks, in-house extensions) are also surfaced — the detector cannot tell them apart from upstream-removed keys, and only the operator can. The y/n/s offer below is the human-in-the-loop step that disambiguates.
Detection
Source the helper and read the deprecated key list:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-detect-deprecated-config.sh"
DEPRECATED=$(detect_deprecated_config_keys)
Return values:
- Empty → nothing to surface, skip to step 9.
- One or more newline-separated key names → continue.
The helper:
- Reads only top-level keys (whole-block removals; sub-key renames are out of scope per the ticket).
- Whitelists metadata keys with a leading underscore (
_comment,_schema_version,_team_comment, etc.) — those aren't deprecated config blocks. - Returns silently with exit 1 if
jqis missing or defaults file is absent (skill should skip detection in that case, not fail).
Offer
If DEPRECATED is non-empty, format and print:
ApexYard /update detected N config block(s) in .claude/project-config.json
that no longer exist in upstream defaults:
- voice_prompts
- abandoned_block
These keys may be:
(a) dead config from a block the framework removed upstream (e.g.
voice_prompts after #157), or
(b) custom extension keys you've added intentionally.
The detector can't tell them apart — choose:
[y] yes, remove the listed keys from .claude/project-config.json
[n] no, leave them alone (they're harmless; you can clean up later)
[s] show me the keys + their current values before deciding
Read the operator's reply.
| Reply | Action |
|---|---|
y | Back the file up first (cp .claude/project-config.json .claude/project-config.json.bak), then run remove_deprecated_config_keys (edits .claude/project-config.json in place, no commit). Print Removed N keys. Backup at .claude/project-config.json.bak — compare with: diff .claude/project-config.json.bak .claude/project-config.json. Do NOT git add the file (me2resh/apexyard#1031): it is gitignored and untracked, so staging exits 1 and git diff --staged would show nothing anyway. A plain-file backup is the reviewable artefact here, because git holds no copy to diff against — which is exactly why an unrecoverable edit needs one. |
n | Print Leaving override untouched. Re-run /update later if you change your mind. and continue to step 9. |
s | Run show_deprecated_config_keys (prints each key + current value), then re-prompt with the same y/n options (no s recursion). |
The skill never auto-removes without explicit y. The skill never auto-commits — staging is the contract, the operator owns the commit.
Why advisory, not destructive
A custom-extension key indistinguishable from an upstream-removed key is a real possibility (e.g. an adopter who's ahead of defaults with their own block). The cost of incorrectly removing a custom block is much higher than the cost of one extra prompt — the y/n/s pattern matches the rest of /update's "operator owns each material change" stance.
8a. Migrate to split-portfolio v2 layout (advisory, default-yes)
Detection. After the merge / rebase has applied the new _lib-portfolio-paths.sh + _lib-ops-root.sh, source the helper and check for two conditions that together identify a pre-v2 split-portfolio adopter:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
# Already v2 (or single-fork) — no migration needed.
if portfolio_is_v2; then
V2_NEEDED=0
elif ! jq -e '.portfolio.registry' .claude/project-config.json >/dev/null 2>&1; then
# No portfolio block at all → single-fork mode → no migration.
V2_NEEDED=0
else
# Has a portfolio block (split-portfolio) but no .apexyard-fork marker
# → pre-v2 split-portfolio adopter. Migration applies.
V2_NEEDED=1
fi
If V2_NEEDED=0 → skip this step entirely and continue to step 9.
If V2_NEEDED=1, present the offer:
ApexYard /update detected your fork is in split-portfolio mode (v1 layout):
- apexyard.projects.yaml → resolved to a sibling private repo (good)
- projects/ → resolved to a sibling private repo (good)
- onboarding.yaml → still in this public fork (v1 layout)
- workspace/ → still in this public fork (v1 layout)
Split-portfolio v2 (introduced in framework #242) moves onboarding.yaml
AND workspace/ to the private sibling repo too, so the public fork holds
ONLY framework files + your customisations to skills/hooks/rules.
Migrate now? This will:
- COPY onboarding.yaml to the sibling private repo (sibling becomes
canonical) + untrack it from the public fork (snapshot left on disk)
- MOVE workspace/<name>/ contents to the sibling private repo
- Add gitignore entries for both in the public fork
- Write a .apexyard-fork marker (the v2 ops-fork anchor)
- Add portfolio.{onboarding,workspace_dir} keys to .claude/project-config.json
onboarding.yaml is COPIED (not moved) — the file is small, the legacy
ops-root walk still reads it as a fallback anchor, and a public-fork
snapshot is a useful safety net while the sibling-repo copy becomes
the source of truth. workspace/ is MOVED — clones are gigabytes; we
don't double disk. See AgDR-0021 § "v1→v2 migration semantics".
Idempotent — if interrupted, re-run.
[Y / n / dry-run — show commands, don't execute]
If --dry-run was passed to /update, force the dry-run branch automatically (print the commands the migration would run, do not execute, then continue to step 9).
Per-file-class confirmation — ask separately for onboarding.yaml and workspace/, so the operator can migrate one and defer the other:
Copy onboarding.yaml to sibling private repo? [Y/n]
Move workspace/? [Y/n] # surfaces disk size: du -sh workspace
Migration steps
For each file class the operator confirmed, run the moves below. Resolve the sibling repo dir from the existing portfolio.registry path (the parent dir of the registry file is the sibling repo root):
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 498
- Forks
- 271
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
update-me2resh- Source
- github.com/me2resh/apexyard