repomatic.components module

Declarative registry of all components managed by the init subcommand.

Every resource the init subcommand can create, sync, or merge is declared here as a Component subclass instance in the COMPONENTS tuple. Each component carries all its metadata: what kind it is, whether it is selected by default, which files it manages, and any per-file properties like repo-scope gating or config keys.

All derived constants (REUSABLE_WORKFLOWS, BARE_INIT_COMPONENTS and the others) are computed from this single registry at the bottom of this module.

class repomatic.components.InitDefault(*values)[source]

Bases: Enum

How init treats the component when no explicit CLI args are given.

INCLUDE = 1

Included by default (like changelog or workflows).

EXCLUDE = 2

In the default set, but excluded unless [tool.repomatic] include names it (like labels or skills).

EXPLICIT = 3

Only included when explicitly requested (like most tool configs).

property in_bare_init: bool

Whether a bare repomatic init considers the component at all.

EXCLUDE components are considered, then excluded unless the user includes them. EXPLICIT components are never considered.

class repomatic.components.SyncMode(*values)[source]

Bases: Enum

How a ToolConfigComponent behaves when the section already exists.

BOOTSTRAP = 1

Insert once, skip if section already exists (like ruff or pytest).

ONGOING = 2

Replace template content on every sync, preserving local additions (like bumpversion).

components()[source]

Every registered tool config this mode governs, in registry order.

Return type:

tuple[ToolConfigComponent, ...]

class repomatic.components.RepoScope(*values)[source]

Bases: Enum

Which repository types a component or file entry applies to.

The classification has three axes: whether the repo is an awesome-* list, whether it carries a PEP 621 pyproject.toml, and whether that project is a distributable package. The first is mutually exclusive with the other two (awesome repos are content lists, not Python projects), so a single scope value suffices.

The Python axis is deliberately split in two. PYTHON_ONLY covers anything that needs Python code to be useful; PACKAGE_ONLY covers only what needs something to publish. A uv virtual project ([tool.uv] package = false) sits between the two: it locks dependencies and runs tests, but never ships a release. Collapsing the pair would hand every blog and docs site a PyPI publish action and a release workflow it can never run.

The workflow layer mirrors the same split. repomatic.pyproject.is_python_project() is the default gate, since sync-uv-lock and sync-dep-sources apply to a virtual project too. repomatic.pyproject.is_python_package() narrows a job to what has something to publish or version: sync-bumpversion is the one that needs it.

Scope restrictions are defaults: they apply during bare repomatic init but are bypassed when components are explicitly named on the CLI or covered by [tool.repomatic] include.

ALL = 1

Included in every repository type.

AWESOME_ONLY = 2

Only for awesome-* repositories.

PYTHON_ONLY = 3

Only for Python projects (PEP 621 [project].name present).

Use for anything a uv virtual project still wants: dependency locking, coverage config, test tooling.

PACKAGE_ONLY = 4

Only for Python projects that build a distributable package.

Strictly narrower than PYTHON_ONLY, excluding uv virtual projects. Use for the release lane: publishing, tagging, changelog upkeep.

matches(traits)[source]

Whether this scope applies to a repository with traits.

Parameters:

traits (RepoTraits) – What init read off the target repository.

Return type:

bool

class repomatic.components.RepoTraits(is_awesome=False, is_python=False, is_package=False, is_source=False)[source]

Bases: object

The traits of a repository that decide which RepoScope applies.

repomatic init reads them off the target tree once per run, and every scope check of that run asks the same instance.

is_awesome: bool = False

Whether the repository is an awesome-* curated list.

is_python: bool = False

Whether pyproject.toml declares a PEP 621 [project] table, per repomatic.pyproject.is_python_project().

is_package: bool = False

Whether that project also builds a distributable package, per repomatic.pyproject.is_python_package(). Always implies is_python.

is_source: bool = False

Whether the repository is the upstream source checkout, per repomatic.upstream.is_source_repo(). No scope reads it: init reads it to never flag the bundled source files as stale.

class repomatic.components.FileEntry(source, target='', file_id='', scope=RepoScope.ALL, config_key='', reusable=True, phase='', tree=False)[source]

Bases: object

A single file managed within a component.

source: str

Filename in repomatic/data/, or a directory when tree is set.

target: str = ''

Relative output path in the target repository. Defaults to source (root-level file).

file_id: str = ''

Identifier for file-level --include/--exclude. Defaults to the filename portion of target.

scope: RepoScope = 1

Which repository types get this file.

config_key: str = ''

[tool.repomatic] key that gates this entry.

Resolved by is_enabled(), so it names a key the user writes, like debug.sync. A key that names no field raises.

reusable: bool = True

Workflow-specific: supports workflow_call trigger.

phase: str = ''

Skill-specific: lifecycle phase for list-skills display.

tree: bool = False

Whether source and target name directories, not files.

A tree entry is copied wholesale, so a skill can ship scripts/, references/ and assets/ alongside its SKILL.md exactly as the Agent Skills spec describes, with no per-file registration.

Caution

Under repomatic/data/ a tree’s directories must be real and only its leaves may be symlinks back into the authoritative tree. uv_build refuses a symlinked directory in package data (Is a directory (os error 21)) and fails the whole wheel, while symlinked files are dereferenced into it normally.

is_enabled(config)[source]

Whether config enables this entry.

An entry with no config_key is always enabled.

Parameters:

config (Config) – The resolved [tool.repomatic] configuration.

Return type:

bool

class repomatic.components.Component(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='')[source]

Bases: object

Base class for all init components.

name: str

Component name used on the CLI (like "skills").

description: str

Human-readable description for help text.

init_default: InitDefault = 1

How init treats this component when no explicit CLI selection is made.

scope: RepoScope = 1

Which repository types get this component. Checked at the component level during auto-exclusion, complementing the file-level FileEntry.scope.

files: tuple[FileEntry, ...] = ()

File entries this component manages.

config_key: str = ''

[tool.repomatic] key that gates this component.

Resolved like FileEntry.config_key.

ephemeral: bool = False

Whether this component’s files are inputs regenerated on demand rather than repository content.

Every consumer of an ephemeral component dumps it right before reading it, so a copy in the working tree is never the one that gets used. Bare repomatic init therefore skips these components, and [tool.repomatic] include cannot opt into materializing them: only naming the component explicitly on the CLI (repomatic init labels) writes its files out, which is how sync-labels stages labels.toml into a temporary directory to hand to labelmaker, leaving the working tree untouched.

location_field: str = ''

Config field holding this component’s destination, when the user can move it.

Set for every component whose destination is configurable: the directories subagents and skills write into, and the single file plugin merges into. Declared targets are built against the default location, so a repo that overrode it needs each target rebased onto the configured one. resolve_target() performs that rebase, and leaving this empty means the targets are fixed (.github/workflows/ is GitHub’s, not ours to move).

property file_ids: frozenset[str]

The identifiers a component/file selector may name.

Empty for a component without file-level selection, like a tool config.

entry(file_id)[source]

The file entry file_id names, or None when none does.

Return type:

FileEntry | None

target_of(file_id)[source]

The declared output path of file_id, or None when none matches.

Return type:

str | None

is_enabled(config)[source]

Whether config enables this component.

A component with no config_key is always enabled.

Parameters:

config (Config) – The resolved [tool.repomatic] configuration.

Return type:

bool

resolve_target(target, config)[source]

Rebase a declared target path onto this component’s configured location.

A no-op unless location_field is set and the resolved config actually moves the destination, so every caller can route every target through this method instead of testing the component name first.

Handles both shapes a location may take. A directory location rebases the path under it; a file location (plugin) is the path, so it is replaced outright. Matching only the directory shape would leave a moved file reported at its default path, and stale-file detection would then hunt for an orphan the repository never wrote there.

Parameters:
  • target (str) – A path as declared on a FileEntry (or a RemovedAsset tombstone), relative to the repository root and expressed against the default location.

  • config (Config | None) – The resolved configuration. None keeps every target at its default location.

Return type:

str

Returns:

The target rebased onto the configured location, or target unchanged.

class repomatic.components.BundledComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='')[source]

Bases: Component

Files copied from repomatic/data/ to a target path.

class repomatic.components.WorkflowComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='')[source]

Bases: Component

Thin-caller generation and header sync.

class repomatic.components.ToolConfigComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='', source_file='', tool_section='', sync_mode=SyncMode.BOOTSTRAP, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False)[source]

Bases: Component

Merged into pyproject.toml.

Note

Nothing here declares where the section lands. init appends it to the [tool] table, then format-pyproject moves it: pyproject-fmt sorts [tool.*] by its own known-tool order, which no per-component hint can override.

source_file: str = ''

Filename in repomatic/data/.

tool_section: str = ''

The [tool.X] section name to check for existence.

sync_mode: SyncMode = 1

How this config behaves when the section already exists.

BOOTSTRAP: insert once, skip if the section is present. ONGOING: re-derive the section from the template on every sync while preserving local additions: keys the template omits, extra items in shared arrays, and extra keys in shared nested tables. The template wins on shared scalars; preserved_keys flips that for named top-level keys.

customizable_entries: tuple[tuple[str, str | None], ...] = ()

Template entries every repository is expected to replace with its own.

Only meaningful when sync_mode is BOOTSTRAP, where check_bootstrap_config_drift() reports what a seeded section never received. A placeholder the template tells the reader to change is absent from every correctly customized section, so reporting it would put a permanent warning on the one knob the template asked the repository to own.

Each entry is (dotted_path, None) for a whole key, or (dotted_path, member) for one item of a list.

legacy_table: str = ''

Sub-table the tool reads in place of the section’s own keys.

Only meaningful when sync_mode is BOOTSTRAP. pytest reads [tool.pytest.ini_options] as well as the native [tool.pytest] keys the template carries, which need pytest 9.0, and refuses a file holding both. check_bootstrap_config_drift() measures a section that keeps its keys in this sub-table against the sub-table, so it never reports them missing or asks for a native twin the tool would reject.

preserved_keys: tuple[str, ...] = ()

Top-level keys whose existing values survive an ongoing sync.

Only meaningful when sync_mode is ONGOING. During replacement, these keys keep their value from the existing config rather than being overwritten by the template placeholder.

graft_identity_keys: tuple[str, ...] = ()

Keys that identify the “slot” of an array-of-tables entry during a graft.

Only meaningful when sync_mode is ONGOING. When set, a local array-of-tables entry that shares its identity tuple (the values of these keys) with a template entry is treated as a stale copy of that canonical entry: the template wins and the local entry is dropped rather than appended as a duplicate. Local entries whose identity matches no template entry are genuinely local and survive. Leave empty to fall back to a plain union-by-value, which cannot tell an evolved canonical entry apart from a new local one.

For bumpversion, the slot is (filename | glob | key_path, replace): filename/glob/key_path name the target file and replace names what the entry writes there, so a stale entry whose search pattern evolved (like one gaining a regex anchor) still maps to the same slot.

overlay: bool = False

Treat the template as a partial section owning only its own keys.

Only meaningful when sync_mode is ONGOING. The default rebuild-and-graft sync rebuilds the whole section from the template and grafts local additions after it, so template keys always land first. That is wrong for a section the project mostly owns and a formatter reorders: [tool.uv], whose keys pyproject-fmt sorts into a fixed schema order. Emitting the owned keys as a leading block would lose to pyproject-fmt on the next format pass and churn an endless sync PR.

With overlay set, an ongoing sync instead updates only the template’s top-level keys in place within the existing section (the template value wins), preserving the existing key order and leaving every other key untouched. The merged section is therefore already a pyproject-fmt fixpoint. A repo missing an owned key has it appended; pyproject-fmt canonicalizes that one position once, after which steady-state syncs are no-ops.

property tool_name: str

The bare tool name, without the tool. table prefix.

The key this component’s section sits under inside a parsed [tool] table, derived once here rather than re-spelled by every consumer of tool_section.

class repomatic.components.TemplateComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='')[source]

Bases: Component

Directory tree (awesome-template).

class repomatic.components.GeneratedComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', ephemeral=False, location_field='', target='')[source]

Bases: Component

Produced from code (changelog).

Unlike bundled components, generated components have no files tuple. The target field records the output path so the auto-exclusion logic can detect stale copies on disk.

target: str = ''

Relative output path in the target repository.

class repomatic.components.RemovedAsset(component, target, removed_in, hashes=(), owned_dir='', successor='')[source]

Bases: object

An asset repomatic once shipped and has since dropped.

Note

Stale-file detection in init only inspects files still listed in COMPONENTS. An asset removed from the registry (a renamed or consolidated skill, a retired workflow) becomes invisible to it, so downstream repos accumulate one orphan per upstream removal. Each RemovedAsset is a tombstone that lets init find and prune those orphans.

init finds an on-disk orphan and decides whether to prune it with one of two gates, depending on the component:

  • Content-gated (skills, agents, config files): the file is deleted only when its normalized content matches one of hashes (a version repomatic shipped), proving it is an untouched copy.

  • Fingerprint-gated (workflows): thin-callers are parameterized per repo (version pin, paths: filters), so they carry no fixed content. The file is deleted only when it is a repomatic-lineage thin-caller for this workflow (its uses: line references an upstream slug, see UPSTREAM_REPO_SLUGS) with no extra downstream jobs.

Either way, a locally modified orphan is reported for manual review, never deleted. When target is already gone but the asset shipped as a folder, an empty owned_dir left behind is pruned on its own: it carries nothing anyone could lose.

component: str

Component the asset belonged to (like "skills" or "workflows").

target: str

Relative output path the asset occupied, in default-location form (like .claude/skills/papaya-report/SKILL.md or .github/workflows/papaya.yaml).

Build skill and subagent targets with _skill_target / _subagent_target so they match the live registry: the skills.location and subagents.location overrides are re-applied at detection time. Workflow targets are literal (.github/workflows/ is fixed by GitHub).

removed_in: str

Bare package version that first stopped shipping the asset (like 7.11.0). Surfaced in the prune report.

hashes: tuple[str, ...] = ()

Content gate for skills, agents and config files: the digest() of every distinct content repomatic shipped for this asset. An on-disk file whose content has any of these digests is an untouched copy of some released version and is safe to delete. Listing one digest per distinct released revision (not just the last) means a downstream repo that synced an older version is still recognized and pruned rather than flagged for review.

Empty for workflows, which are fingerprint-gated by their uses: line instead (see the class docstring).

owned_dir: str = ''

Directory the asset had to itself, in default-location form (like .claude/skills/papaya-report), for an asset shipped as a folder.

A skill is a folder, so deleting its SKILL.md by any route other than init (a hand rm, a repomatic old enough to unlink the file alone) leaves the folder behind, empty. target no longer exists, so the tombstone never fires again and the fossil outlives every later init. Declaring the folder gives detection a second thing to look for. Empty for an asset that shipped as a lone file in a shared directory (a subagent, a workflow), whose parent must never be swept.

successor: str = ''

Optional human note describing what replaced the asset, shown in the report (like replaced by repomatic-ship).

property content_gated: bool

Whether an orphan is judged by its content rather than its uses: line.

Every asset is content-gated except a workflow, whose thin caller carries no fixed content to compare.

static digest(text)[source]

The hex SHA-256 of text in the form init writes it to disk.

The text is normalized first, through normalize_text(), so trailing whitespace a repository added after the write never reads as a local change.

Parameters:

text (str) – The content of a file, as shipped or as found on disk.

Return type:

str

is_untouched(text)[source]

Whether text is the content of a released revision of this asset.

Parameters:

text (str) – The content of the orphan found on disk.

Return type:

bool

locate(path, config)[source]

Rebase path, in default-location form, onto the configured location.

Applies the same *.location override as the live targets of component, through Component.resolve_target(). A dropped component the registry no longer lists leaves path unchanged.

Parameters:
Return type:

str

repomatic.components.SKILL_FILENAME = 'SKILL.md'

Name the Agent Skills spec reserves for a skill’s entry point.

repomatic.components.SKILL_SOURCE_ROOT = 'skills'

Directory under repomatic/data/ holding one folder per bundled skill.

repomatic.components.COMPONENTS: tuple[Component, ...] = (BundledComponent(name='labels', description='Label definitions for labelmaker (labels.toml)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='labels.toml', target='labels.toml', file_id='labels.toml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False),), config_key='', ephemeral=True, location_field=''), BundledComponent(name='publish-pypi-action', description='Composite action that publishes to PyPI via Trusted Publishing (.github/actions/publish-pypi/)', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.PACKAGE_ONLY: 4>, files=(FileEntry(source='action-publish-pypi.yaml', target='.github/actions/publish-pypi/action.yaml', file_id='action.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False),), config_key='', ephemeral=False, location_field=''), BundledComponent(name='subagents', description='Agent subagent definitions (.claude/agents/)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='agent-grunt-qa.md', target='.claude/agents/grunt-qa.md', file_id='grunt-qa', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='agent-qa-engineer.md', target='.claude/agents/qa-engineer.md', file_id='qa-engineer', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='agent-sphinx-docs.md', target='.claude/agents/sphinx-docs.md', file_id='sphinx-docs', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False)), config_key='', ephemeral=False, location_field='subagents_location'), BundledComponent(name='skills', description='Claude Code skill definitions (.claude/skills/)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='skills/av-false-positive', target='.claude/skills/av-false-positive', file_id='av-false-positive', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/awesome-triage', target='.claude/skills/awesome-triage', file_id='awesome-triage', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/babysit-ci', target='.claude/skills/babysit-ci', file_id='babysit-ci', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/benchmark-update', target='.claude/skills/benchmark-update', file_id='benchmark-update', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/brand-assets', target='.claude/skills/brand-assets', file_id='brand-assets', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/file-bug-report', target='.claude/skills/file-bug-report', file_id='file-bug-report', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/github-housekeeping', target='.claude/skills/github-housekeeping', file_id='github-housekeeping', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/probe-workflow', target='.claude/skills/probe-workflow', file_id='probe-workflow', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/repomatic-audit', target='.claude/skills/repomatic-audit', file_id='repomatic-audit', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/repomatic-changelog', target='.claude/skills/repomatic-changelog', file_id='repomatic-changelog', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/repomatic-deps', target='.claude/skills/repomatic-deps', file_id='repomatic-deps', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/repomatic-init', target='.claude/skills/repomatic-init', file_id='repomatic-init', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Setup', tree=True), FileEntry(source='skills/repomatic-ship', target='.claude/skills/repomatic-ship', file_id='repomatic-ship', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/repomatic-test-matrix', target='.claude/skills/repomatic-test-matrix', file_id='repomatic-test-matrix', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/repomatic-topics', target='.claude/skills/repomatic-topics', file_id='repomatic-topics', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/repomatic-upgrade', target='.claude/skills/repomatic-upgrade', file_id='repomatic-upgrade', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/sphinx-docs-sync', target='.claude/skills/sphinx-docs-sync', file_id='sphinx-docs-sync', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/translation-sync', target='.claude/skills/translation-sync', file_id='translation-sync', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/upstream-audit', target='.claude/skills/upstream-audit', file_id='upstream-audit', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True)), config_key='', ephemeral=False, location_field='skills_location'), WorkflowComponent(name='workflows', description='Thin-caller workflow files', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='autofix.yaml', target='.github/workflows/autofix.yaml', file_id='autofix.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='autolock.yaml', target='.github/workflows/autolock.yaml', file_id='autolock.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='cancel-runs.yaml', target='.github/workflows/cancel-runs.yaml', file_id='cancel-runs.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='changelog.yaml', target='.github/workflows/changelog.yaml', file_id='changelog.yaml', scope=<RepoScope.PACKAGE_ONLY: 4>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='debug.yaml', target='.github/workflows/debug.yaml', file_id='debug.yaml', scope=<RepoScope.PYTHON_ONLY: 3>, config_key='debug.sync', reusable=True, phase='', tree=False), FileEntry(source='docs.yaml', target='.github/workflows/docs.yaml', file_id='docs.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='labels.yaml', target='.github/workflows/labels.yaml', file_id='labels.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='lint.yaml', target='.github/workflows/lint.yaml', file_id='lint.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='metrics.yaml', target='.github/workflows/metrics.yaml', file_id='metrics.yaml', scope=<RepoScope.ALL: 1>, config_key='metrics.sync', reusable=True, phase='', tree=False), FileEntry(source='_release-engine.yaml', target='.github/workflows/release.yaml', file_id='release.yaml', scope=<RepoScope.PACKAGE_ONLY: 4>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='tests.yaml', target='.github/workflows/tests.yaml', file_id='tests.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=False, phase='', tree=False), FileEntry(source='unsubscribe.yaml', target='.github/workflows/unsubscribe.yaml', file_id='unsubscribe.yaml', scope=<RepoScope.ALL: 1>, config_key='notification.unsubscribe', reusable=True, phase='', tree=False)), config_key='', ephemeral=False, location_field=''), TemplateComponent(name='awesome-template', description='Boilerplate for awesome-* repositories', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.AWESOME_ONLY: 2>, files=(), config_key='awesome-template.sync', ephemeral=False, location_field=''), GeneratedComponent(name='changelog', description='Minimal changelog.md', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.PACKAGE_ONLY: 4>, files=(), config_key='', ephemeral=False, location_field='changelog_location', target='changelog.md'), GeneratedComponent(name='plugin', description='Claude Code plugin marketplace wiring (.claude/settings.json)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='settings_location', target='.claude/settings.json'), ToolConfigComponent(name='uv', description='uv resolver pin and dependency cooldown policy', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='uv.toml', tool_section='tool.uv', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=True), ToolConfigComponent(name='lychee', description='Lychee link checker configuration', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.AWESOME_ONLY: 2>, files=(), config_key='', ephemeral=False, location_field='', source_file='lychee.toml', tool_section='tool.lychee', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='ruff', description='Ruff linter/formatter configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='ruff.toml', tool_section='tool.ruff', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='pytest', description='Pytest test configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='pytest.toml', tool_section='tool.pytest', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(('addopts', '--cov=.'),), legacy_table='ini_options', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='coverage', description='Coverage.py measurement and reporting configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='coverage.toml', tool_section='tool.coverage', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='mypy', description='Mypy type checking configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='mypy.toml', tool_section='tool.mypy', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='mdformat', description='mdformat Markdown formatter configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='mdformat.toml', tool_section='tool.mdformat', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), ToolConfigComponent(name='bumpversion', description='bump-my-version configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='bumpversion.toml', tool_section='tool.bumpversion', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=('current_version',), graft_identity_keys=('filename', 'glob', 'key_path', 'replace'), overlay=False), ToolConfigComponent(name='typos', description='Typos spell checker configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='typos.toml', tool_section='tool.typos', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False))

The component registry.

Single source of truth for all resources managed by the init subcommand. Every component declares its kind, selection default, file entries, and behavioral flags. All derived constants are computed from this tuple.

repomatic.components.COMPONENTS_BY_NAME: dict[str, Component] = {'awesome-template': TemplateComponent(name='awesome-template', description='Boilerplate for awesome-* repositories', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.AWESOME_ONLY: 2>, files=(), config_key='awesome-template.sync', ephemeral=False, location_field=''), 'bumpversion': ToolConfigComponent(name='bumpversion', description='bump-my-version configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='bumpversion.toml', tool_section='tool.bumpversion', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=('current_version',), graft_identity_keys=('filename', 'glob', 'key_path', 'replace'), overlay=False), 'changelog': GeneratedComponent(name='changelog', description='Minimal changelog.md', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.PACKAGE_ONLY: 4>, files=(), config_key='', ephemeral=False, location_field='changelog_location', target='changelog.md'), 'coverage': ToolConfigComponent(name='coverage', description='Coverage.py measurement and reporting configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='coverage.toml', tool_section='tool.coverage', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'labels': BundledComponent(name='labels', description='Label definitions for labelmaker (labels.toml)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='labels.toml', target='labels.toml', file_id='labels.toml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False),), config_key='', ephemeral=True, location_field=''), 'lychee': ToolConfigComponent(name='lychee', description='Lychee link checker configuration', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.AWESOME_ONLY: 2>, files=(), config_key='', ephemeral=False, location_field='', source_file='lychee.toml', tool_section='tool.lychee', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'mdformat': ToolConfigComponent(name='mdformat', description='mdformat Markdown formatter configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='mdformat.toml', tool_section='tool.mdformat', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'mypy': ToolConfigComponent(name='mypy', description='Mypy type checking configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='mypy.toml', tool_section='tool.mypy', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'plugin': GeneratedComponent(name='plugin', description='Claude Code plugin marketplace wiring (.claude/settings.json)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='settings_location', target='.claude/settings.json'), 'publish-pypi-action': BundledComponent(name='publish-pypi-action', description='Composite action that publishes to PyPI via Trusted Publishing (.github/actions/publish-pypi/)', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.PACKAGE_ONLY: 4>, files=(FileEntry(source='action-publish-pypi.yaml', target='.github/actions/publish-pypi/action.yaml', file_id='action.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False),), config_key='', ephemeral=False, location_field=''), 'pytest': ToolConfigComponent(name='pytest', description='Pytest test configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='pytest.toml', tool_section='tool.pytest', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(('addopts', '--cov=.'),), legacy_table='ini_options', preserved_keys=(), graft_identity_keys=(), overlay=False), 'ruff': ToolConfigComponent(name='ruff', description='Ruff linter/formatter configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='ruff.toml', tool_section='tool.ruff', sync_mode=<SyncMode.BOOTSTRAP: 1>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'skills': BundledComponent(name='skills', description='Claude Code skill definitions (.claude/skills/)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='skills/av-false-positive', target='.claude/skills/av-false-positive', file_id='av-false-positive', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/awesome-triage', target='.claude/skills/awesome-triage', file_id='awesome-triage', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/babysit-ci', target='.claude/skills/babysit-ci', file_id='babysit-ci', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/benchmark-update', target='.claude/skills/benchmark-update', file_id='benchmark-update', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/brand-assets', target='.claude/skills/brand-assets', file_id='brand-assets', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/file-bug-report', target='.claude/skills/file-bug-report', file_id='file-bug-report', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/github-housekeeping', target='.claude/skills/github-housekeeping', file_id='github-housekeeping', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/probe-workflow', target='.claude/skills/probe-workflow', file_id='probe-workflow', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/repomatic-audit', target='.claude/skills/repomatic-audit', file_id='repomatic-audit', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/repomatic-changelog', target='.claude/skills/repomatic-changelog', file_id='repomatic-changelog', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/repomatic-deps', target='.claude/skills/repomatic-deps', file_id='repomatic-deps', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/repomatic-init', target='.claude/skills/repomatic-init', file_id='repomatic-init', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Setup', tree=True), FileEntry(source='skills/repomatic-ship', target='.claude/skills/repomatic-ship', file_id='repomatic-ship', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Release', tree=True), FileEntry(source='skills/repomatic-test-matrix', target='.claude/skills/repomatic-test-matrix', file_id='repomatic-test-matrix', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Quality', tree=True), FileEntry(source='skills/repomatic-topics', target='.claude/skills/repomatic-topics', file_id='repomatic-topics', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Development', tree=True), FileEntry(source='skills/repomatic-upgrade', target='.claude/skills/repomatic-upgrade', file_id='repomatic-upgrade', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/sphinx-docs-sync', target='.claude/skills/sphinx-docs-sync', file_id='sphinx-docs-sync', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/translation-sync', target='.claude/skills/translation-sync', file_id='translation-sync', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', reusable=True, phase='Maintenance', tree=True), FileEntry(source='skills/upstream-audit', target='.claude/skills/upstream-audit', file_id='upstream-audit', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='Maintenance', tree=True)), config_key='', ephemeral=False, location_field='skills_location'), 'subagents': BundledComponent(name='subagents', description='Agent subagent definitions (.claude/agents/)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='agent-grunt-qa.md', target='.claude/agents/grunt-qa.md', file_id='grunt-qa', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='agent-qa-engineer.md', target='.claude/agents/qa-engineer.md', file_id='qa-engineer', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='agent-sphinx-docs.md', target='.claude/agents/sphinx-docs.md', file_id='sphinx-docs', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False)), config_key='', ephemeral=False, location_field='subagents_location'), 'typos': ToolConfigComponent(name='typos', description='Typos spell checker configuration', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='typos.toml', tool_section='tool.typos', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=False), 'uv': ToolConfigComponent(name='uv', description='uv resolver pin and dependency cooldown policy', init_default=<InitDefault.EXPLICIT: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='', ephemeral=False, location_field='', source_file='uv.toml', tool_section='tool.uv', sync_mode=<SyncMode.ONGOING: 2>, customizable_entries=(), legacy_table='', preserved_keys=(), graft_identity_keys=(), overlay=True), 'workflows': WorkflowComponent(name='workflows', description='Thin-caller workflow files', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='autofix.yaml', target='.github/workflows/autofix.yaml', file_id='autofix.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='autolock.yaml', target='.github/workflows/autolock.yaml', file_id='autolock.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='cancel-runs.yaml', target='.github/workflows/cancel-runs.yaml', file_id='cancel-runs.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='changelog.yaml', target='.github/workflows/changelog.yaml', file_id='changelog.yaml', scope=<RepoScope.PACKAGE_ONLY: 4>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='debug.yaml', target='.github/workflows/debug.yaml', file_id='debug.yaml', scope=<RepoScope.PYTHON_ONLY: 3>, config_key='debug.sync', reusable=True, phase='', tree=False), FileEntry(source='docs.yaml', target='.github/workflows/docs.yaml', file_id='docs.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='labels.yaml', target='.github/workflows/labels.yaml', file_id='labels.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='lint.yaml', target='.github/workflows/lint.yaml', file_id='lint.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='metrics.yaml', target='.github/workflows/metrics.yaml', file_id='metrics.yaml', scope=<RepoScope.ALL: 1>, config_key='metrics.sync', reusable=True, phase='', tree=False), FileEntry(source='_release-engine.yaml', target='.github/workflows/release.yaml', file_id='release.yaml', scope=<RepoScope.PACKAGE_ONLY: 4>, config_key='', reusable=True, phase='', tree=False), FileEntry(source='tests.yaml', target='.github/workflows/tests.yaml', file_id='tests.yaml', scope=<RepoScope.ALL: 1>, config_key='', reusable=False, phase='', tree=False), FileEntry(source='unsubscribe.yaml', target='.github/workflows/unsubscribe.yaml', file_id='unsubscribe.yaml', scope=<RepoScope.ALL: 1>, config_key='notification.unsubscribe', reusable=True, phase='', tree=False)), config_key='', ephemeral=False, location_field='')}

Index for O(1) component lookup by name.

repomatic.components.REMOVED_ASSETS: tuple[RemovedAsset, ...] = (RemovedAsset(component='codecov', target='.github/codecov.yaml', removed_in='7.8.0', hashes=('e8e96bfead62334599f4ec4c0448f2376352629789a70a76ae6fc3746ff7057b',), owned_dir='', successor='coverage is now gated by pytest --cov-fail-under'), RemovedAsset(component='labels', target='.github/labeller-content-based.yaml', removed_in='7.11.0', hashes=('1f3e670c0b4c6687a8920fb3738a15fb82b8639b7825d81f76c55bc5784cdb08', 'adf62c78c539229d34d4d2518a9af7f39df44d599c60852784b0faa47a6defa9', '5cf481b4aec2bf98a4056757f41ef5fc50f808dbd7c8a43f1dea0b224ecb7f1f', '8a047d53d5449ea0b53517e2f63e126360050127342084b7a705f34fb735d818'), owned_dir='', successor='rules now live in repomatic.github.labels.DEFAULT_CONTENT_RULES'), RemovedAsset(component='labels', target='.github/labeller-file-based.yaml', removed_in='7.11.0', hashes=('9dc0948e23a3a83d2cec5f11e400c75992fb1ce326eb6c5811c1fc3bfe258b31', 'b216d370e4d2c6118f46d9bb2eacaf91392e6a6267a4f4857f44a698422cc860', '9a4feeb49c37ee7eba1d13957d26aaaa867c791ec12be8cd4197e7526bfbf963'), owned_dir='', successor='rules now live in repomatic.github.labels.DEFAULT_FILE_RULES'), RemovedAsset(component='workflows', target='.github/workflows/renovate.yaml', removed_in='7.0.0', hashes=(), owned_dir='', successor='replaced by self-hosted sync-tool-versions, sync-action-pins, and sync-workflow-pins'))

Tombstones for assets repomatic has dropped (see RemovedAsset).

init prunes orphaned copies of these from downstream repos. Ordered by (component, target). A tombstone stays for the major releases a downstream repository may still upgrade from: the ones of assets dropped before 7.0.0 are gone.

When you drop a bundled asset from COMPONENTS, add an entry here so the removal propagates downstream on the next init instead of leaving an orphan. List one digest per distinct content the asset shipped across its released lifetime, collected from the release tags where its data file existed:

import subprocess

from repomatic.components import RemovedAsset

src = "repomatic/data/skills/papaya-report/SKILL.md"  # The dropped data file.
tags = subprocess.run(
    ["git", "tag", "--list", "v*"],
    capture_output=True, text=True, encoding="UTF-8", check=True,
).stdout.split()
digests = {}
for tag in tags:
    blob = subprocess.run(
        ["git", "show", f"{tag}:{src}"],
        capture_output=True, text=True, encoding="UTF-8",
    )
    if blob.returncode == 0:
        digests.setdefault(RemovedAsset.digest(blob.stdout), tag)
print(tuple(digests))  # Distinct contents, in first-shipped order.

A skill ships as a folder: give its SKILL.md as target and its folder as owned_dir, both through _skill_target and _skill_dir. Removed workflows are fingerprint-gated, not hashed: declare them through _removed_workflow, which omits hashes.

repomatic.components.BARE_INIT_COMPONENTS: frozenset[str] = frozenset({'awesome-template', 'changelog', 'labels', 'lychee', 'plugin', 'publish-pypi-action', 'skills', 'subagents', 'workflows'})

Components a bare repomatic init considers, before any exclusion.

repomatic.components.DEFAULT_EXCLUDED_COMPONENTS: frozenset[str] = frozenset({'labels', 'plugin', 'skills', 'subagents'})

Components a bare repomatic init excludes unless the user includes them.

repomatic.components.EPHEMERAL_COMPONENTS: frozenset[str] = frozenset({'labels'})

Components whose files are regenerated on demand (see Component.ephemeral).

A bare repomatic init excludes them, and [tool.repomatic] include cannot bring them back: only naming one on the CLI writes its files out.

repomatic.components.EPHEMERAL_TARGETS: frozenset[str] = frozenset({'labels.toml'})

Target paths belonging to Component.ephemeral components.

Written only when the component is named explicitly on the CLI, and never worth committing: whatever reads them regenerates them first. init uses this to keep its closing “commit the generated files” advice off a run that produced nothing but scratch output.

repomatic.components.BUNDLED_VERBATIM_TARGETS: frozenset[str] = frozenset({'.claude/agents/grunt-qa.md', '.claude/agents/qa-engineer.md', '.claude/agents/sphinx-docs.md', '.claude/skills/av-false-positive', '.claude/skills/awesome-triage', '.claude/skills/babysit-ci', '.claude/skills/benchmark-update', '.claude/skills/brand-assets', '.claude/skills/file-bug-report', '.claude/skills/github-housekeeping', '.claude/skills/probe-workflow', '.claude/skills/repomatic-audit', '.claude/skills/repomatic-changelog', '.claude/skills/repomatic-deps', '.claude/skills/repomatic-init', '.claude/skills/repomatic-ship', '.claude/skills/repomatic-test-matrix', '.claude/skills/repomatic-topics', '.claude/skills/repomatic-upgrade', '.claude/skills/sphinx-docs-sync', '.claude/skills/translation-sync', '.claude/skills/upstream-audit', '.github/actions/publish-pypi/action.yaml', 'labels.toml'})

Target paths repomatic init writes verbatim from a repomatic/data/ template.

Every BundledComponent copies its bundled source byte-for-byte to the target, so downstream the file’s content (including any SHA-pinned uses: ref) is owned by repomatic init. sync-action-pins and sync-workflow-pins skip these paths for the same reason they skip UPSTREAM_REPO_SLUGS: a pin the next sync-repomatic overwrites turns the two pull requests into a ping-pong, the bump PR and the init-revert PR chasing each other. The skip lifts inside the source repo, where each bundled source is a symlink to its in-tree target and the pin is a normal source-of-truth ref (see repomatic.deps.sync._pinnable_files). Generated workflows (WorkflowComponent) are deliberately absent: they carry only upstream-slug refs (already skipped) and may host downstream-authored extra jobs whose third-party pins the bumpers should keep current.

repomatic.components.REUSABLE_WORKFLOWS: tuple[str, ...] = ('autofix.yaml', 'autolock.yaml', 'cancel-runs.yaml', 'changelog.yaml', 'debug.yaml', 'docs.yaml', 'labels.yaml', 'lint.yaml', 'metrics.yaml', 'release.yaml', 'unsubscribe.yaml')

Workflow filenames that support workflow_call triggers.

repomatic.components.NON_REUSABLE_WORKFLOWS: frozenset[str] = frozenset({'tests.yaml'})

Workflows without workflow_call that cannot be used as thin callers.

repomatic.components.ALL_WORKFLOW_FILES: tuple[str, ...] = ('autofix.yaml', 'autolock.yaml', 'cancel-runs.yaml', 'changelog.yaml', 'debug.yaml', 'docs.yaml', 'labels.yaml', 'lint.yaml', 'metrics.yaml', 'release.yaml', 'tests.yaml', 'unsubscribe.yaml')

All workflow filenames (reusable and non-reusable).

repomatic.components.WORKFLOW_SOURCES: dict[str, str] = {'autofix.yaml': 'autofix.yaml', 'autolock.yaml': 'autolock.yaml', 'cancel-runs.yaml': 'cancel-runs.yaml', 'changelog.yaml': 'changelog.yaml', 'debug.yaml': 'debug.yaml', 'docs.yaml': 'docs.yaml', 'labels.yaml': 'labels.yaml', 'lint.yaml': 'lint.yaml', 'metrics.yaml': 'metrics.yaml', 'release.yaml': '_release-engine.yaml', 'tests.yaml': 'tests.yaml', 'unsubscribe.yaml': 'unsubscribe.yaml'}

Maps each workflow’s downstream file_id to its bundled source filename.

For most workflows source == file_id. The release entry is the exception: its downstream artifact is release.yaml, whose backing reusable engine is _release-engine.yaml (the lane the generic “is this a reusable workflow” tests inspect). The full set of reusable lanes the generated release.yaml calls is RELEASE_ENGINE_WORKFLOWS.

repomatic.components.RELEASE_ENGINE_WORKFLOWS: tuple[str, ...] = ('_release-build.yaml', '_release-engine.yaml')

Reusable workflows the generated release.yaml references but that repomatic init never materializes downstream.

The workflows component deploys a generated release.yaml (not a thin delegation): its build job calls _release-build.yaml and its release job calls _release-engine.yaml, each via {repo}/.github/workflows/<lane>@<tag> resolved from this repo at the release tag rather than copied into the downstream tree. These lanes live in .github/workflows/ here (and at every release tag) but are not FileEntry targets and never appear in ALL_WORKFLOW_FILES.

The release entry’s FileEntry still records _release-engine.yaml as its source (see WORKFLOW_SOURCES) so the generic backing-reusable tests and a downstream repomatic lint can read it via get_data_content to check the engine lane forwards its secrets; _release-build.yaml is not bundled because nothing reads it at runtime (the build lane declares no secrets). Naming both lanes here lets stale-file detection and the data-symlink rules treat them as a group instead of special-casing each by hand.

repomatic.components.SELF_MAINTENANCE_WORKFLOWS: frozenset[str] = frozenset({'self-maintenance.yaml'})

Workflows that maintain this package’s own source and never ship downstream.

Unlike RELEASE_ENGINE_WORKFLOWS, which downstream repos still reach remotely through a uses: ref at a release tag, these are invisible outside this repository: they are not FileEntry targets, carry no repomatic/data/ symlink, and nothing resolves them at runtime. That is what lets their jobs drop the github.repository == 'kdeldycke/repomatic' guard every in-autofix.yaml upstream-only step needs, and pick a schedule without spending downstream CI.

A workflow belongs here when its write domain is a path that exists only in this repository (repomatic/tooling/registry.py and friends). A workflow that merely behaves differently upstream does not: it still ships, so it still needs the runtime guard.

repomatic.components.FILE_SELECTOR_COMPONENTS: tuple[str, ...] = ('labels', 'publish-pypi-action', 'subagents', 'skills', 'workflows')

Components that support file-level component/file selectors.

repomatic.components.parse_component_entries(entries, *, context='entry')[source]

Parse component entries into full-component and file-level sets.

Bare names (no /) must be component names from COMPONENTS_BY_NAME. Qualified component/identifier entries target individual files. Raises ValueError on unknown entries.

Used by both the exclude config path and the CLI positional selection, with context controlling error message wording.

Parameters:

context (str) – Label for error messages (like "exclude" or "selection").

Return type:

tuple[set[str], dict[str, set[str]]]

Returns:

(full_components, file_selections) where file_selections maps component names to sets of file identifiers.