-
Notifications
You must be signed in to change notification settings - Fork 0
Extracting a Component into an Independent Repository
This guide describes how to extract a component from an established Git repository without losing its useful history, ownership boundaries, or ability to operate as an independent project. The component can be a code module, SDK, command-line tool, documentation set, asset collection, or another unit with its own lifecycle.
The migration of shared Agent Skills from
aigengame/godot-agent to
aigengame/skills provides the worked
example. Agent Skills are specific to this case. The migration method is not.
This guide uses these terms:
- Source repository: the repository that owns the component before extraction.
- Target repository: the independent repository that will own the component.
- Extracted component: the files, history, and responsibilities that move.
- Consumer: a project or user that depends on the extracted component.
The goal is not to preserve original commit IDs. A history rewrite necessarily creates new commit IDs. The goal is to preserve the relevant changes, authorship, timestamps, and ancestry in a traceable new history. Preserve commit messages unless their repository-local meaning requires a deliberate, recorded rewrite.
This guide does not prescribe one package manager or consumer integration method. Those choices depend on the type of component and the needs of its consumers.
A repository split is not only a file move. It changes four boundaries:
- Ownership: which repository is authoritative for each artifact.
- History: which changes remain traceable after the split.
- Validation: how the target repository proves that it is internally valid.
- Delivery: how consumers obtain a known revision or release.
A directory can be contiguous without being a valid repository boundary. Before the migration, check whether the proposed component has:
- an independent reason to change;
- a release or update cadence that differs from the source product;
- one or more consumers outside the source repository;
- a stable interface or artifact boundary;
- validation that can run without hidden source-repository state;
- a clear owner after extraction.
Do not define the boundary by file extension or directory name alone. Two files with the same format can have different owners and release constraints.
The shared workflow skills could evolve independently of godot-agent, so they
moved to aigengame/skills. The gda product skill at
src/gda/skill/SKILL.md stayed in godot-agent because it is version-locked to the
installed CLI, and this migration did not introduce an independent compatibility
and coordinated-release contract. Moving it without such a contract would have
separated product guidance from the product version that makes the guidance true.
The general rule is:
Split by lifecycle and authority, not by surface similarity.
Creating an independent source repository and choosing how consumers integrate it are separate decisions.
A consumer can use a package manager, a source dependency, a Git submodule, vendoring, a generated artifact, a binary release, or another mechanism. The target repository should define what it publishes and what compatibility it promises. It should not control a consumer's directory layout unless that layout is part of the public contract.
The initial plan mounted the complete aigengame/skills repository as a submodule
at each consumer's project-level skill root. The design provided an exact Git commit,
but it also caused three problems:
- The submodule owned the complete destination directory, so the consumer could not add private content there without modifying the submodule worktree.
- A normal clone did not populate the submodule. Every developer and CI job needed an explicit recursive clone or initialization step.
- Repository infrastructure, such as CI, tests, and maintenance scripts, appeared beside the content that the consumer wanted.
A symlink composition layer preserved more consumer control, but added platform and harness differences and required separate link maintenance. The target repository therefore adopted a content-aware installer as its supported delivery path. Consumers still remain free to select another integration method when it fits their constraints.
The reusable lesson is broader than this case:
Do not confuse a repository checkout with a consumer-ready payload.
Create an extraction inventory before rewriting history. At minimum, record:
- the exact source commit;
- a deterministic content baseline that matches the extraction mapping;
- every historical path used by the component;
- important file, directory, and commit counts;
- incoming and outgoing code or build dependencies;
- generated files, Git LFS objects, submodules, and large assets;
- licenses and notices that must follow the component;
- issue, pull request, and commit-message references;
- current tests, build commands, and consumer smoke tests;
- source tags that should or should not move.
For a code module, also record its public API, package metadata, dependency lock state, and build artifact. For documentation or structured content, record the parser, schema, link rules, and discovery contract.
For one intact subtree, its Git tree object ID is a suitable content baseline. For
multiple roots or a deliberate path rearrangement, record one tree ID per source
root, a canonical git ls-tree manifest, or a synthetic tree that applies the
planned path mapping. The baseline must cover every extracted path.
Use immutable evidence instead of a moving branch name. The worked example extracted one intact subtree and recorded the following values in the repository-owned migration record:
Source commit:
04d3e089b32330aeffc7da98f4824bb21873c2b3
Source subtree:
130cc305354d03daedfac1c7e0573eb2be4b2ab5
Target repository base:
6ad2baf0504e2f83b3a4a87516c3cefdfb5c2ad0
The commit fixes the point in time. The tree object ID fixes the exact directory snapshot at that point in time. A multi-root extraction needs the equivalent multi-root or mapped baseline described above.
The current path is often not the complete history boundary. A component can move through several directories before extraction.
Use repository history to find renames and earlier locations:
git log --follow -- <file>
git log --all --name-status -- <path>
git log --all --name-status --find-renames--follow helps with one file. It does not infer every directory-level history path
for a later filter operation. Enumerate those paths explicitly.
In the worked example, the content first existed under .claude/skills/ and later
moved to .agents/skills/. Filtering only the current path would have removed the
earlier changes.
This problem occurs frequently in code repositories. A package that is now under
packages/foo/ might previously have existed under src/foo/, libs/foo/, or
internal/foo/.
Run the extraction in a fresh, disposable clone at the pinned source commit. Do not rewrite history in a daily development worktree.
Use a fixed version of the history-rewrite tool. The worked example used
git-filter-repo 2.47.0 through an isolated uvx environment:
uvx --from 'git-filter-repo==2.47.0' git-filter-repo \
--path .agents/skills/ \
--path .claude/skills/ \
--path-rename .agents/skills/: \
--path-rename .claude/skills/: \
--message-callback \
'return re.sub(br"(?<![\w/#])#([0-9]+)\b", br"aigengame/godot-agent#\1", message)'This command performed three operations:
- It retained only the component's current and historical paths.
- It mapped those paths to one location in the target history.
- It qualified bare issue references that would otherwise point to the wrong repository after the move.
Review mixed-purpose commits after filtering. If one source commit changed both the component and unrelated source code, the filtered commit retains only the component part. Its original message can then describe work that no longer exists in that commit. Rewrite or document such messages when they become misleading.
Also account for signed history. Rewritten commits have new object IDs, so original commit signatures cannot remain valid for the rewritten objects. Record this effect if signed commits or signed tags are part of the repository's trust model.
A repository move can change the meaning of metadata even when the text does not change.
For example, a source commit message that contains #123 refers to issue 123 in the
source repository. In the target repository, the same text refers to a different
issue. The worked migration rewrote these references to
aigengame/godot-agent#123.
Do not apply an unrestricted #N replacement to source files or documentation. The
same syntax can be a heading, color, protocol field, or example. Limit automatic
rewrites to a context where the original meaning is known, then inspect the result.
The same review applies to:
- relative documentation links;
- repository badges;
- CODEOWNERS paths;
- CI workflow paths;
- package and container names;
- security-reporting links;
- references to source-repository releases or branches.
If the target repository has no commits, the filtered history can become its initial history. If the target already contains initialization commits, the histories have no common ancestor.
Join two unrelated histories with an explicit merge commit:
git merge \
--allow-unrelated-histories \
--no-ff \
--no-commit \
<filtered-head>
git commitThe resulting migration commit has two parents:
target initialization history ----+
+-- migration merge commit
filtered component history -------+
Do not squash or rebase this migration pull request. A squash keeps the current file snapshot but collapses the filtered commit sequence. A rebase creates another set of rewritten commits and invalidates evidence pinned to the filtered head. Neither method makes the reviewed filtered head an ancestor of the target default branch.
This is a one-time exception, not a permanent merge policy. After the migration, ordinary pull requests can use squash-only rules. Apply or tighten those rules after the history import, or configure an explicit exception for the migration.
The presence of files in the target repository proves only that a snapshot arrived. Use independent checks for content, ancestry, object integrity, metadata, and scope.
For one subtree that maps intact to the filtered root, compare the source subtree with the root tree of the filtered history. Equal tree object IDs prove that relative paths, file modes, and blob content are identical at that boundary.
For multiple source roots or rearranged paths, compare the filtered result with the
baseline selected during inventory. This can be a set of per-root tree IDs plus an
explicit path map, a canonical git ls-tree manifest, or a synthetic mapped tree.
Do not claim lossless extraction from one subtree ID when that ID covers only part of
the component.
In the worked example, both tree IDs were:
130cc305354d03daedfac1c7e0573eb2be4b2ab5
Perform structural changes after this comparison in separate commits. Later path changes can be valid, but they should not obscure whether the initial extraction was lossless.
Verify that the filtered head is reachable from the target branch:
git merge-base --is-ancestor <filtered-head> HEADThis check detects an import that copied the files but lost the filtered ancestry through a squash or an incorrect merge.
Run:
git fsck --full --strictThis checks the consistency of reachable Git objects and references. It does not prove that the extracted component works, so it must not replace component tests.
Verify or sample:
- author and committer identity;
- author and committer dates;
- commit messages;
- file-level history across old paths;
- expected commit and file counts;
- absence of unrelated product paths;
- absence of source tags that were not part of the target release model;
- absence of credentials and internal configuration.
A pure rename commit can become empty when both its old and new paths map to the same target path. Its removal is valid if it contains no remaining state change. In the worked example, one path-only rename was pruned while the three earlier commits with content changes remained.
Run the final checks in a full, non-shallow clone of the remote target repository. This removes local worktree state and unreachable local objects from the evidence.
The final worked-example audit found:
- 29 retained component commits;
- 13 component directories and 23 migrated files at the import boundary;
- equal source and filtered trees;
- a reachable filtered head;
- no imported source tags;
- a clean
git fsck --full --strictresult.
History import creates a repository. It does not create an independently maintainable project.
Add the smallest set of capabilities that makes the target usable:
- a README that states purpose, scope, and exclusions;
- the licenses and notices that apply to the extracted component;
- contribution and ownership guidance;
- one documented local validation path;
- CI that enforces the component's real invariants;
- a version source when consumers need stable revisions;
- a release or delivery path when a Git commit alone is not sufficient.
Keep governance proportional to current needs. A new repository does not automatically need issue templates, multiple bots, a large policy hierarchy, or a custom framework.
An early release check compared only the dynamically discovered number of skills. A different name set with the same count could pass. The check was replaced with an exact comparison against the dynamically discovered name set, including a same-count, wrong-name counterexample.
The validator also replaced regular-expression Markdown parsing with a CommonMark parser. Reference links, fenced code, and escaped syntax made the regular expression an unreliable release gate.
The reusable rules are:
- Validate the identity and properties that must remain true. A count alone does not establish set equality.
- Use a parser that implements the actual data format when the check is a merge or release gate.
- Separate an environment-tooling failure from a component failure.
An extracted component can use commit pins, Git tags, semantic versions, package versions, or another release identifier. Select the smallest model that gives its consumers the required reproducibility.
If release automation uses commit categories, define those categories from behavior. Do not infer release impact from file extensions.
In the worked example, the product artifacts are Markdown files. A change to a
SKILL.md workflow can be a feature or a fix even though the file extension is
.md. Conversely, a CI or test-only change does not need a content release merely
because it is in the same repository.
This rule also applies to schemas, templates, generated-code definitions, policy files, and configuration. A text change can alter a public contract.
Repository tests prove only what they execute. They do not prove that a consumer can obtain and use the published result.
Add a smoke test at the delivery boundary:
| Extracted component | Representative consumer check |
|---|---|
| Library | Install it in an empty project and call one public API |
| CLI | Download the release artifact and run a bounded command |
| Container | Pull the published digest and start the service |
| SDK | Compile and run a minimal consumer project |
| Documentation set | Build it with the production renderer and check links |
| Structured content | Load it with the real discovery or parsing tool |
The worked example used the real Skills CLI to list and install the repository at an exact Git tag. It compared the installed payload with the release candidate and confirmed that repository infrastructure did not enter the consumer payload.
This check demonstrates a general delivery sequence:
repository contents -> release artifact, if any -> installed content
Do not assume that these forms are equal. Verify each transition that exists in the delivery chain.
Do not require every consumer to move at the same time as the source history.
A safer sequence is:
- Import and verify the component history in the target repository.
- Make the target independently testable and releasable.
- Publish a known revision.
- Move consumers in separate, reversible changes.
- Remove duplicate source content only after the required consumers pass.
This sequence keeps rollback simple. Before consumer cutover, the source repository still contains the known working copy. After cutover, each consumer change can be reverted without rewriting either repository's history.
For code modules, a temporary compatibility layer or duplicated package can be reasonable if it has an owner, an exit condition, and a tracked removal task. Avoid an indefinite dual-authority state.
Do not combine the history import, structural refactoring, CI design, release automation, and all consumer migrations in one change.
The worked migration used separate pull requests for:
- target repository scope and onboarding;
- history import;
- validation and CI;
- minimal governance;
- target directory organization;
- delivery documentation;
- release automation;
- the first release.
This sequence kept tree equality meaningful at the import boundary. Later structure changes did not obscure the initial extraction evidence.
For a code split, use the same pattern: import behavior without change first, then change package boundaries, APIs, dependencies, and releases in separately testable pull requests.
| Failure mode | Cause | Smallest effective response |
|---|---|---|
| The target has the files but not their history | The migration copied or squashed the snapshot | Import filtered history through a reachable merge commit |
| Early changes are missing | Only the current path was filtered | Enumerate and filter all historical paths |
| Issue links point to the wrong repository | Bare references kept repository-local meaning | Qualify references during the controlled metadata rewrite |
| Consumer-local files conflict with the dependency | The dependency owns a complete destination directory | Give the consumer a composition point or use a content-aware installer |
| Unrelated repository files reach consumers | A checkout was treated as the delivery payload | Define and test an explicit packaging or installation boundary |
| Validation accepts the wrong set | An early check compares only the discovered count | Compare the exact dynamically discovered identity set |
| Valid documents fail a link check | A regular expression approximates a formal syntax | Use the real parser for the format |
| A tool is blamed for an environment failure | The test assumes unavailable package or cache tooling | Reproduce in a declared, isolated environment and classify the failure |
| Release notes omit product changes | Commit type follows file extension instead of behavior | Define release semantics from consumer-visible impact |
| The migration is declared complete at merge time | No remote or consumer validation follows the merge | Verify a fresh clone, release revision, and real consumer path |
- Identify the component's owner, lifecycle, consumers, and public contract.
- List files that look related but must remain with the source product.
- Decide which repository owns each fact after the split.
- Keep delivery and consumer-layout decisions separate from source ownership.
- Record the source commit and a content baseline that covers every extracted path.
- Inventory historical paths, dependencies, tests, tags, and references.
- Record a known working build or validation result.
- Define rollback and consumer cutover conditions.
- Use a disposable, non-shallow clone at the pinned revision.
- Pin the history-rewrite tool.
- Filter all historical paths.
- Rewrite only metadata whose meaning would otherwise change.
- Inspect mixed-purpose commits and signed-history consequences.
- Compare the filtered result with the single-root or mapped multi-root baseline.
- Inspect author, date, message, and file-history retention.
- Check for unrelated paths, tags, large files, and secrets.
- Run
git fsck --full --strict. - Run the component's tests or format-specific validators.
- Use the filtered history directly if the target is empty.
- Otherwise join the histories with an explicit two-parent merge commit.
- Do not squash or rebase the migration pull request.
- Record the source, filtered, target, and merge revisions in the pull request.
- Prove that the filtered head is an ancestor of the target default branch.
- Add minimal onboarding, licensing, ownership, validation, and CI.
- Add versioning and release automation only when consumers need them.
- Keep repository governance proportional to actual maintenance needs.
- Obtain the artifact from the same remote path that consumers use.
- Pin an exact revision, tag, version, or digest.
- Run a minimal real-consumer test.
- Compare candidate, published, and installed content where applicable.
- Move each consumer in a separate reversible change.
- Track temporary compatibility or duplication explicitly.
- Remove the old source only after required consumers pass.
- Reconcile documentation, issue tracking, branch rules, and ownership records.
- The split follows lifecycle and authority rather than path or file type.
- Product-version-bound content remains with the product, or the extraction defines an explicit compatibility and coordinated-release contract.
- Source and target responsibilities have one authoritative home each.
- The target does not impose an unnecessary consumer layout.
- The source commit and a complete single-root or mapped multi-root content baseline are recorded.
- All historical component paths are included.
- Cross-repository references still identify the correct repository.
- Rewritten-history and signature effects are disclosed.
- The filtered head is reachable from the target default branch.
- The migration pull request was not squashed or rebased.
- The filtered result equals the recorded content baseline at the import boundary.
-
git fsck --full --strictpasses in a fresh clone. - Unrelated paths and unwanted source tags are absent.
- Metadata retention has been verified or sampled.
- Component tests or validators pass in a declared environment.
- Purpose, scope, exclusions, and contribution paths are documented.
- CI checks the component's stable invariants.
- The version source is clear when versions are required.
- A real consumer can obtain and use an exact published revision.
- Release artifacts, when present, and installed content exclude infrastructure that is outside the declared delivery contract.
- Consumer migrations and old-copy removal are tracked separately.
- The rollback path remains usable until cutover succeeds.
- Acceptance criteria were checked after remote publication, not only after merge.
- Remaining validation gaps and risks are recorded.
The aigengame/skills migration produced an independent repository with preserved
component history, repository-owned validation, CI, and release automation.
The initial history import retained 29 component commits. The first independent
release was v0.1.0,
at commit 1e1f0d182730c42bc2dbdc99c750b83684846e1c.
The migration was delivered incrementally through repository initialization, history import, catalog validation, minimal governance, delivery documentation, directory organization, release automation, and the first release pull request.
The final result was not only a new repository URL. The extracted component gained an independent lifecycle while its history remained traceable, its releases became verifiable, and its consumers retained control of their own integration model.