Skip to content

Extracting a Component into an Independent Repository

haihong.qin edited this page Sep 1, 2026 · 1 revision

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.

Terms and scope

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.

1. Treat the split as a boundary migration

A repository split is not only a file move. It changes four boundaries:

  1. Ownership: which repository is authoritative for each artifact.
  2. History: which changes remain traceable after the split.
  3. Validation: how the target repository proves that it is internally valid.
  4. 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.

Case example: a file that stayed in the source repository

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.

2. Separate source ownership from consumer integration

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.

Case example: why the default submodule design was rejected

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.

3. Inventory dependencies and history before the migration

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.

4. Find all historical paths

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/.

5. Rewrite history in a disposable clone

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:

  1. It retained only the component's current and historical paths.
  2. It mapped those paths to one location in the target history.
  3. 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.

6. Preserve repository references deliberately

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.

7. Join the filtered history to the target repository

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 commit

The 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.

8. Verify more than file presence

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.

8.1 Compare the source and filtered content baselines

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.

8.2 Prove ancestry

Verify that the filtered head is reachable from the target branch:

git merge-base --is-ancestor <filtered-head> HEAD

This check detects an import that copied the files but lost the filtered ancestry through a squash or an incorrect merge.

8.3 Check the object graph

Run:

git fsck --full --strict

This 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.

8.4 Inspect metadata and scope

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.

8.5 Repeat the checks in a fresh clone

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 --strict result.

9. Make the target repository operationally independent

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.

Case example: validate identity, not only quantity

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.

10. Define version meaning from behavior, not file type

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.

11. Validate the real consumer path

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.

12. Keep history import and consumer cutover as separate phases

Do not require every consumer to move at the same time as the source history.

A safer sequence is:

  1. Import and verify the component history in the target repository.
  2. Make the target independently testable and releasable.
  3. Publish a known revision.
  4. Move consumers in separate, reversible changes.
  5. 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.

13. Use small pull requests with distinct evidence

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:

  1. target repository scope and onboarding;
  2. history import;
  3. validation and CI;
  4. minimal governance;
  5. target directory organization;
  6. delivery documentation;
  7. release automation;
  8. 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.

14. Common failure modes

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

15. Reusable migration procedure

Phase 1: Define the boundary

  • 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.

Phase 2: Pin the evidence

  • 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.

Phase 3: Rehearse the extraction

  • 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.

Phase 4: Verify the filtered result

  • 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.

Phase 5: Import the history

  • 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.

Phase 6: Make the target independent

  • 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.

Phase 7: Validate delivery

  • 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.

Phase 8: Move consumers and retire the old copy

  • 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.

16. Acceptance checklist

Boundary

  • 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.

History

  • 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.

Integrity

  • The filtered result equals the recorded content baseline at the import boundary.
  • git fsck --full --strict passes 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.

Independent operation

  • 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.

Completion

  • 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.

17. Worked-example outcome

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.

Clone this wiki locally