Syncs shared, generic tooling between this repo and its parent template repo via
/template. The parent is configured in properties.yml (stamped automatically by
inv setup.properties when GitHub's generated-from link is available):
template:
local: "$HOME/path/to/parent-template-repo"
remote: "github.com/<user>/<parent-template-repo>"root skeleton → domain template → project repo
Repos form a chain, and each repo syncs only with its direct parent. A domain template absorbs
the root skeleton's updates by running /template inside itself (its own properties.yml
points at the root skeleton) — never from further down the chain. To move a change across the whole
chain, sync it one hop at a time: push it up to the parent, then run /template push inside
the parent to continue upward (or pull downward hop by hop).
uv run --no-sync python -m modules.template.route "pull resolve" # pull, phase 1
uv run --no-sync python -m modules.template.route "pull copy" # pull, phase 2 (local only)
uv run --no-sync python -m modules.template.route "push diff" # push, phase 1/template (the prompt) drives the flow above directly. tasks/template.py also exposes thin
invoke wrappers around the same functions, for manual use outside the prompt:
uv run --no-sync invoke template.pull
uv run --no-sync invoke template.pull_copy
uv run --no-sync invoke template.push_diff
uv run --no-sync invoke template.push_apply --files=modules/foo.py,tasks/foo.py --deletes=modules/old.py
uv run --no-sync invoke template.push_create_pr --branch=sync-from-repo-20260101-000000 --title="..." --body="..."pull resolve resolves template.local from properties.yml. If that path exists on disk, it's
used directly — no git pull is run, since the assumption is you already pushed your changes
there before running /template here. If the local path isn't found (e.g. a different machine or
CI), it shallow-clones template.remote into tmp/template_sync/ instead. Either way, it prints
TEMPLATE_PATH=<path> and TEMPLATE_LOCAL=<true|false> so the /template prompt can parse them
out of any other output.
When the template repo is local (TEMPLATE_LOCAL=true), pull copy does the actual sync
deterministically — no AI-driven diffing:
- Lists every git-tracked file in the parent (
git ls-files, so the parent's own gitignored/ untracked junk -- caches, build artifacts,.venv/, etc. -- is skipped automatically; nothing about that is hardcoded here). - Drops anything matched by this project's
template.ignore.ymlexclude:list -- the only other exclusion mechanism, covering everything project-specific the parent has no equivalent of (properties.yml,README.md, business modules, personal-vault content, and its own filename so local customizations survive future pulls). - Overwrites every remaining file outright — text files get the parent's name rewritten to this
repo's name via
modules/template/naming.py(the same rename helperpushuses, in reverse); binary files copy as-is. No per-file diff or confirmation: the working tree is git-tracked, so the result is reviewed withgit status/git diffafterward, not gated before the write.
When the template repo isn't local, or the user explicitly asks for a manual/diff review instead,
the file-by-file comparison, classification (shared tooling vs. project-specific), and conflict
resolution happens in the /template prompt itself, not here — see
.github/prompts/template.prompt.md.
Proposes new generic improvements from this repo (in modules/, .github/instructions/,
.github/prompts/, .claude/commands/, .claude/skills/, .clinerules/workflows/) into the
parent template repo as a pull request. Candidates come from git ls-files (so this repo's own
.gitignore is already applied) filtered again by template.ignore.yml -- the same file pull
uses, applied in reverse so project-specific content (business modules, personal-vault content,
...) never leaks upstream. See modules/template/scope.py.
Three phases, split at the confirmation boundary so the agent can check in with the user between each step:
push diff— enumerates and diffs the scoped candidates against the parent template repo, printingADDED:/MODIFIED:/DELETED:lists.push apply --file <path> ... [--delete <path> ...]— for the changes the user approved: fetches/updates the parent's default branch, creates a newsync-from-<repo>-<timestamp>branch, copies the files in (and applies approved deletions), commits, and pushes the branch. Does not open a PR.push create-pr --branch <name> [--title ...] [--body ...]— opens the PR viagh pr create. Confirms with the user first in interactive runs.
Repo-name references are rewritten on copy (this repo's name → the parent's name, both derived
from properties.yml repo.local/template.local basenames), so name-only differences never
need hand-editing.
uv run --no-sync python -m modules.template.route ["pull <mode>" | "push <mode> ..."]
↓
modules/template/route.py
↓ ↓
modules/template/pull.py modules/template/push.py
↓ resolve ↓ copy ↓ diff / apply / create-pr
│ modules/template/ignore.py modules/template/scope.py
│ (template.ignore.yml only (INCLUDE_DIRS + git ls-files +
│ -- .gitignore is handled template.ignore.yml via ignore.py)
│ via git ls-files, not
│ hardcoded)
│ ↓
│ modules/template/naming.py ←──────────────────┘
│ (shared repo-name rewrite, used by both pull and push)
↓ ↓
modules/template/resolve.py ←──────────────────────────────────────┘
(shared local-path-or-clone resolution)
↓
properties.yml (template.local / template.remote) → TEMPLATE_PATH=<resolved path>