Files
holocron/plugins/git/.apm/skills/git-submodules/SKILL.md
Defame1297 5e232503c4 feat(kyberforge): execute plugin-to-apm marketplace conversion
Why:
ADR-0015 established that Microsoft APM (apm.yml + .apm/) should replace
this repo's hand-authored plugin.json/marketplace.json model, with those
files becoming compiled output of `apm pack` instead of files edited by
hand via the (now-retired) plugin-author/marketplace-author skills.
Issue #90 was the deferred execution of that decision, gated on #88
(apm tooling) and #89 (apm-native agent-author/skill-author routing).

Implementation notes:
- All six plugins (bin, core, git, gitea, kyberforge, lint) now carry
  apm.yml + .apm/{skills,agents,hooks} as their authoring source. Skills
  moved with a plain git mv (content-identical across targets). Agents
  were re-authored, not moved: per ADR-0016, .apm/agents/*.agent.md
  compiles verbatim to both Claude and Copilot, so plugin-scope agents
  now carry only name/description/model/source_keys -- no tools: field,
  no Claude-only knobs (isolation, maxTurns, effort, memory,
  permissionMode).
- Root apm.yml registers all 7 marketplace packages (6 local plus
  mattpocock-skills as a remote entry) under versioning: per_package,
  matching this repo's existing independent-plugin-versioning practice.
- .claude-plugin/marketplace.json and every plugin's plugin.json are now
  apm-pack-compiled output, verified against the prior hand-maintained
  content: same names/descriptions/versions/licenses/authors, only
  cosmetic serialization differences (JSON key order, owner email vs.
  url, Unicode escaping).
- plugin-author and marketplace-author are retired now that apm-based
  authoring fully replaces their job; kyberforge bumped 1.3.1 -> 1.4.0
  for that removal, and the root marketplace catalog bumped
  0.3.1 -> 0.3.2 to match, per the version-bump convention now
  documented in apm-workflow's reference docs instead of a dedicated
  script (apm has no native version-bump automation).
- Fixed hardcoded pre-.apm/ path assumptions across
  .pre-commit-config.yaml, .pre-commit-hooks.yaml,
  scripts/check-scope-walkup-sync.sh, scripts/sync-vale-styles.sh,
  scripts/check-vale-style-sync.sh, six plugins' root plugin.json
  (stale skills/hooks/agents pointer fields that check-manifests.sh
  validates), and several tests/*.bats and tests/*.sh fixtures --
  including a bats REPO_ROOT relative-path depth bug (10 files, one
  extra .apm/ directory level to walk up) and a vale probe-path
  isolation regression introduced mid-fix.
- Corrected empirically-wrong assumptions surfaced this session in
  apm-workflow/apm-install's own reference docs: `apm marketplace
  package add` does not accept local paths (only owner/repo remote
  shorthand -- local packages are registered by editing apm.yml's
  marketplace.packages[] directly); `apm compile` is a consumer-side
  AGENTS.md/CLAUDE.md generator, not the plugin.json producer, and
  hard-fails on skill/agent-only packages without --clean; `apm plugin
  init <name>` nests a stray subdirectory when run with a positional
  name arg from inside a same-named directory; no native Copilot
  marketplace output profile exists; .mcp.json is merged into the
  compiled plugin.json content-aware and target-scoped, with no
  dependencies.mcp entry needed for simple passthrough; pipx is the
  correct pip fallback on externally-managed Python environments.
- Renamed agent-author's copilot.agent.md template asset to
  copilot.agent.md.template so apm compile's recursive *.agent.md glob
  stops misparsing the placeholder template as a real agent primitive.

Impact:
plugin.json and marketplace.json are compiled artifacts from here on --
editing them by hand is no longer the workflow; edit apm.yml/.apm/ and
run apm pack. CONTEXT.md's Plugin/Plugin marketplace glossary entries
reflect this. ADR-0001 is marked superseded, ADR-0006 moot, and
ADR-0010 updated for the new .apm/agents/ path (project/user scope
unaffected, per ADR-0016). Full local verification: claude plugin
validate --strict on all 6 plugins, apm audit --ci, apm marketplace
check, check-manifests.sh, and the full test suite (165/165 bats,
13/13 shell scripts) all pass clean.

Fixes: #90
Refs: #88, #89
ADR: 0015
ADR: 0016

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC
2026-08-12 18:21:24 +00:00

7.6 KiB

name, description, metadata
name description metadata
git-submodules Use when managing Git submodules: add dependencies as submodules, initialize and update nested repositories, sync URLs, inspect status (including detached HEAD and divergence), and safely remove submodules. Handles multi-repo projects with pinning, parallel operations, and recursive traversal. Use for both initial setup and ongoing maintenance workflows, even if the user doesn't explicitly say "submodule". Do not use for general git operations outside of submodule management.
category source_keys
git
git-scm-submodule-docs

Concept

A submodule is a full Git repository embedded as a subdirectory inside a parent repository (the superproject). The superproject doesn't store the submodule's files — it stores a pointer to a specific commit SHA in the submodule's own history, and the two repos keep fully independent commit histories.

Two files govern a submodule, and they serve different audiences:

  • .gitmodules — version-controlled, shared with collaborators. Defines each submodule's name, path, and canonical URL.
  • .git/config — local only, populated by git submodule init. This is where local URL overrides live (e.g. a private mirror) — they never propagate to other clones.

The submodule's own .git directory lives at .git/modules/<name>/ in the superproject, linked to the submodule's working tree via a .git pointer file. After git submodule update, the working tree normally ends up in detached HEAD state — see Gotchas.

Gotchas

  • Detached HEAD by default. git submodule update checks out a specific commit, not a branch. Work on a branch first, then update the pointer in the superproject. Commits made in detached state are invisible until pinned.
  • Two pushes required, in order. Always commit and push the submodule first, then update and push the superproject's pointer. The superproject only stores a commit SHA — if that SHA isn't reachable on the submodule's remote yet, git submodule update fails for anyone who pulls the superproject before the submodule push lands.
  • --recursive is not default. Most commands operate one level deep. Pass --recursive explicitly for nested submodules.
  • .git/modules/ persists after git rm. Manual cleanup is needed: rm -rf .git/modules/<name>/.
  • Detached HEAD detection. Status prefix + means the checked-out commit differs from the superproject's recorded commit — normal after update --remote, but should be re-pinned before committing.
  • Relative URLs resolve against the remote, not the filesystem. A ../foo.git entry in .gitmodules is relative to the superproject's default remote URL.
  • Custom update commands are security-gated. A .gitmodules entry of update = !some-command is never copied to .git/config by git submodule init — this stops a clone from silently executing arbitrary code.

Conventions

  • Use rtk git for parent-repo operations. Drop into the submodule directory only for submodule-specific git commands (committing/pushing inside the submodule itself) — mixing the two from the wrong working directory targets the wrong repo's history.
  • Check for a dirty submodule before committing the parent pointer. After adding or updating a submodule, run git status in both the parent and the submodule. A -dirty suffix means the submodule has uncommitted local changes; committing the parent pointer now would pin a state no one else can reproduce, since those changes exist only in the local working tree.

Operations

  • Clone a repo that has submodules: rtk git clone --recurse-submodules <url> (one step, Git 2.13+) or rtk git clone <url> followed by rtk git submodule update --init --recursive.
  • Add a submodule: rtk git submodule add <url> <path> (-b <branch> to track a branch instead of a pinned commit, --depth 1 for a shallow clone, -f to force past a gitignored path or name conflict, --name <name> when the logical name should differ from the path). Stages a .gitmodules entry and a gitlink — a commit is still required.
  • Initialize: rtk git submodule init [<path>...] copies submodule URLs from .gitmodules to .git/config. This is the point at which local URL overrides can be edited before fetching. Does not clone — use update (or update --init to run both in one step).
  • Update (clone + checkout): rtk git submodule update --init --recursive is the common case — checks out the recorded commit in detached HEAD. Add --remote --merge (or --remote --rebase) to track the branch tip instead, --jobs <n> for parallel clones, -f to discard local changes. Full flag table: references/submodules.md.
  • Inspect status: rtk git submodule status --recursive (add --cached to show SHAs in the superproject index instead of the working tree). Status prefixes: - not initialized, + diverged from the superproject's recorded commit, U merge conflict.
  • Sync and rebind URLs: rtk git submodule sync --recursive after an upstream URL rename propagates .gitmodules changes into .git/config. rtk git submodule set-url <path> <url> changes a URL directly; rtk git submodule set-branch -b <branch> <path> sets the tracking branch used by update --remote.
  • Override a submodule URL locally (private mirror): local-only, doesn't propagate to collaborators, and gets overwritten by the next sync. Full steps: references/submodules.md.
  • Run a command across all submodules: rtk git submodule foreach --recursive '<command>'. Shell variables available inside <command> ($name, $sm_path, $displaypath, $sha1, $toplevel): references/submodules.md.
  • Deinit (unregister without removing): rtk git submodule deinit <path> (--all for every submodule, -f if local modifications are present) clears the .git/config section and empties the working tree. deinit is not removal — the .gitmodules entry and the gitlink in the superproject's index are untouched.
  • Safe removal (destructive; confirm before executing) — full three-step sequence including the manual .git/modules/ cleanup: references/submodules.md.
  • Move an embedded .git into .git/modules/: rtk git submodule absorbgitdirs [<path>...] — needed when a submodule was created or copied without going through git submodule add. Details: references/submodules.md.

Configuration

.gitmodules (version-controlled, shared with collaborators):

Key Purpose
submodule.<name>.path Working tree path
submodule.<name>.url Remote URL
submodule.<name>.branch Branch used by update --remote
submodule.<name>.update Default update procedure
submodule.<name>.shallow Recommend shallow clone

.git/config (local only, populated by init):

Key Purpose
submodule.<name>.url Local URL override
submodule.<name>.update Local procedure override
submodule.fetchJobs Default parallelism for update --jobs
submodule.recurse Auto-recurse submodule updates on pull/push/etc.
rtk git config submodule.recurse true   # keep submodules pinned automatically after every pull

Agent output format

Return results as structured data:

operation: <clone|add|init|update|sync|set-url|set-branch|status|summary|absorbgitdirs|remove>
status: <success|error|partial>
message: <human-readable summary>
details:
  - <submodule-path>: <state>
conflicts: [<submodule-path>, ...]  # if any
next_step: <recovery action if applicable>

For errors, include the git command output and recommend recovery (e.g., git submodule deinit, force-update, or URL override).