This is the steady-state counterpart to the one-time bootstraps. Use it when:
-
Confirm it's a sync case, and capture the base branch. Sync only operates on
a spine steer itself wrote — check the state, don't merely test that spec/
exists:
. "${CLAUDE_PLUGIN_ROOT}/hooks/lib/repo-root.sh"
. "${CLAUDE_PLUGIN_ROOT}/hooks/lib/spine.sh"
root="$(steer_repo_root "$PWD")" && steer_spine_state "$root"
Only damaged (spec/.version present, spine files missing) and
managed are sync cases. unmanaged (no spec/) or foreign (a
spec/ with no spec/.version — e.g. an OpenAPI spec/ this plugin never
created) is not a sync case: stop and redirect — /steer:init (greenfield /
template fork) or /steer:adopt (existing app to reverse-engineer). Never
"reconcile" a directory steer never wrote. Before creating any branch, record
the currently checked-out branch — call it BASE:
BASE=$(git rev-parse --abbrev-ref HEAD)
BASE is the branch the dev invoked the sync from; the sync's PR targets it
(step 8), so the sync lands back onto the work it continues, not main.
If invoked as /steer:sync --check, do not branch or write anything:
run steps 2–6.5 read-only (the migration preview, the capability status table,
and the invocation-hygiene findings) and stop. The rest of this section is the full (writing) flow. Then
branch a feat/sync off BASE and work there — never commit to main or
to BASE directly (commit-autonomy rule). If BASE is main (the dev ran
sync from a clean trunk), that's the one case the PR targets main. Nothing is
committed until the dev approves.
-
Update the plugin first. The ledger and templates this skill reads are
only current if the plugin is. Tell the dev to run
/plugin update steer@e22-plugins if they haven't this session, then
resolve the current plugin version from
${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json — never from memory. Call
this TARGET.
-
Read the repo's stamp. Read /spec/.version for the version the spine was
last materialized or synced at — call it FROM:
grep -m1 -oE '[0-9]+\.[0-9]+\.[0-9]+' spec/.version 2>/dev/null || echo "unstamped"
unstamped means the repo predates stamping (bootstrapped before this
feature) — treat FROM as 0.0.0 and rely on each migration's precondition
to decide what actually applies. If FROM already equals TARGET, there are
no pending migrations; skip to step 5 (additive reconciliation can still find
template drift) — say so rather than going silent.
Establish the repo profile. Read the CLAUDE.md ## Profile marker
(<!-- steer:profile=… -->); absent → app (back-compat). The back-fill
migration in the ledger writes =app when the marker is missing (idempotent —
it fires only while absent). The profile selects which scaffold overlay
steps 5–6 reconcile against (an infra repo reconciles the root infra
mise.toml + infra CI, never package.json/compose.yaml). If the marker
was changed since the last sync, offer the newly-matching overlay
additively — never remove the prior profile's files (clobber-free).
-
Apply pending structural migrations. Open the ledger at
${CLAUDE_PLUGIN_ROOT}/templates/reference/MIGRATIONS.md. Walk its entries
oldest→newest. For each entry whose introducing version is greater than
FROM, check its precondition against the repo; apply the action
only if the precondition holds (entries are idempotent and self-detecting, so
an entry already applied — or never relevant — is a safe no-op). Because the
precondition is the real gate, when FROM is unstamped walk the whole
ledger by precondition. Apply each as the ledger directs — git mv for
renames so history follows, git rm for deletions, or an in-file token
rewrite (replace only the exact old→new string pairs the entry enumerates,
never a broader match) — all read-then-propose, never clobber filled-in
content. For a token-rewrite entry, run its precondition grep first and show
the diff of proposed substitutions before applying. List each migration
you're applying (and each skipped, with why) before touching files.
-
Reconcile the materialized templates (additive). After structural
migrations, run the standard Template reconciliation convention
(${CLAUDE_PLUGIN_ROOT}/templates/reference/SPEC-FRAMEWORK.md §"Template
reconciliation") across the copied-in files this repo has —
PRODUCTIONIZATION.md, each feature's intent.md / contract.md,
tracker.md, app/README.md, and the scaffold files
(.github/workflows/ci.yml, PR template, mise.toml tasks, …): for each, run
that convention's diff command and splice in only what's missing, additive-only
(never overwrite, reorder, or delete a dev/PO-added row). Reference prose
(templates/reference/*) and ADRs are exempt — do not reconcile them (they're
read in place / immutable).
For the scaffold, follow the copy-and-adapt, never clobber discipline from
the scaffold MANIFEST.md: diff and merge into existing files (CI, compose,
config), adapt to the repo's real stack, and never touch working app code.
For the non-Markdown scaffold files the heading/checklist convention can't
parse — .gitignore and the JSON configs (.claude/settings.json,
biome.json, configs/tsconfig.base.json) — reconcile with the
structured helper instead, which is additive and never overwrites an existing
value or line:
# check (read-only): empty output = current; any output = additive delta
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/scaffold_reconcile.py" \
auto .gitignore "${CLAUDE_PLUGIN_ROOT}/templates/scaffold/gitignore"
# apply the additive merge once you've shown the delta
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/scaffold_reconcile.py" \
auto .claude/settings.json \
"${CLAUDE_PLUGIN_ROOT}/templates/scaffold/claude/settings.json" --apply
This is the content-level merge (permission lists, companion-plugin
entries, config keys). For the permissions block it also de-conflicts
across precedence tiers (deny > ask > allow): a pattern that would end up
in two tiers — e.g. a locally allow-listed Bash(git push) meeting the
template's ask copy — is kept only in its most-restrictive tier, so the
merge never leaves a contradictory allow+ask pair (a - line in the
delta shows the dropped copy). The plugin-enablement wiring inside
.claude/settings.json — the steer@e22-plugins marker — is separately
verified by capability repair (step 6); both are additive and the merge here
never flips an existing value, so a deliberate "steer@e22-plugins": false
opt-off is preserved.
No .mcp.json reconcile. The github, markitdown, and context7 MCP
servers ship with the plugin (plugins/steer/.mcp.json), not the scaffold — so they
refresh with the /plugin update in step 2 and are not part of scaffold
reconciliation; there is no scaffold template to diff a repo .mcp.json
against. A repo bootstrapped before v2.11.0 still has the old repo-local
.mcp.json, whose entries now duplicate the plugin's; the v2.11.0 migration
in step 4 removes the redundant copy (or just the duplicated keys, keeping
product-specific servers). Don't reconcile .mcp.json here.
-
Repair capability gaps (missing / mis-wired scaffold wiring). Additive
reconciliation (step 5) only splices into files that already exist and the
ledger only transforms files that exist — so a repo adopted before a
capability shipped (or that lost a wiring file) silently lacks it, and the
sync so far would report "current." Close that here. Run the read-only
detector and walk the capability map:
sh "${CLAUDE_PLUGIN_ROOT}/scripts/scan-capabilities.sh" .
It prints one id<TAB>status<TAB>files line per capability
(present-wired | absent | mis-wired | disabled | n/a) plus a stack
fingerprint, on stdout (gaps are not a nonzero exit). For each capability
read its entry in
${CLAUDE_PLUGIN_ROOT}/templates/reference/CAPABILITIES.md for the repair
semantics + conditionality, then:
present-wired / n/a / disabled → nothing to do. n/a means the
conditional predicate doesn't apply (wrong stack/tracker) — never re-add it.
disabled (a "steer@e22-plugins": false) is a deliberate opt-off — respect
it. There is no opt-out file; a deliberately-dropped always capability will
re-appear as a proposal each sync and the dev declines it.
absent → create the file(s) from the bundled scaffold
(copy-and-adapt per the scaffold MANIFEST.md), adapting to the repo's real
stack. compose.yaml is the exception: its need isn't knowable — propose
only after confirming with the dev, and when uncertain, ask rather than
create.
mis-wired → for verbatim files (the version-pin scripts) re-copy
from the plugin source — but show the diff first and warn that local
edits are lost (move product-specific pins to policy/versions.yml). For
everything else, additively splice only the named wiring marker (the
steer@e22-plugins entry, a CI step, a PR-template section), preserving every
existing key/step — never clobber.
Some repairs need a human/external step sync can't do: branch-protection.yml
is written here but applied server-side by /steer:protect. (claude.yml
needs only the ANTHROPIC_API_KEY secret to run — the marketplace repo is
public, so the plugin clone is anonymous and needs no credential.)
Emit a capability status table (this is the whole output under --check):
| Capability | Files | Status | Action |
|---|---|---|---|
| plugin-enabled-local | .claude/settings.json | mis-wired | splice enabledPlugins.steer (proposed) |
| delivery-mode-declared | CLAUDE.md | mis-wired | splice ## Delivery mode, default pr-flow (proposed); ask if solo-trunk fits |
| in-ci-plugin-loading | .github/workflows/claude.yml | absent | create from scaffold (proposed); needs ANTHROPIC_API_KEY secret |
| version-pin-enforcement | policy/versions.yml, scripts/… | mis-wired | re-copy verbatim scripts (proposed, diff shown) |
| drift-gate | .github/workflows/ci.yml, PR template | present-wired | none |
| branch-protection-policy | policy/branch-protection.yml | absent | create (proposed); apply via /steer:protect |
| github-issue-forms | .github/ISSUE_TEMPLATE/* | n/a | none (tracker ≠ github) |
Under --check, don't branch or write — continue to step 6.5 (invocation
hygiene) and stop there; that is where --check ends, not here. Otherwise
apply the proposed repairs on feat/sync under the read-then-propose discipline
and carry on.
6.5. Repair invocation hygiene (stale / invalid slash invocations in live prose).
A repo's live instruction prose (CLAUDE.md, README.md,
.github/pull_request_template.md) is frozen at the version that wrote it, so a
skill rename, a skill folded into a reference mode, or a skill turned
user-invocable: false leaves invocations that no longer resolve — and Claude
Code has no built-in check that a referenced skill exists. The v2.0.0 ledger
migration (step 4) rewrites the pre-rebrand /e22-* tokens once; this step is the
standing every-sync backstop that also catches the post-rebrand classes and
any later drift. Run the read-only detector:
The detector scans only live instruction surfaces and deliberately skips
append-only/provenance prose (spec/HISTORY.md, spec/reports/*, ADRs, feature
intent.md provenance) — a past e22-adopt mention there is a legitimate record, not
live guidance. --check stops here: print the findings and exit — no writes.