Files
holocron/plugins/git/skills/git-remotes/SKILL.md
Defame1297 38f1ba4e03 fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of
.apm/ nesting -- it convention-scans only flat skills/, agents/, commands/,
hooks.json at each plugin's root. Confirmed via strings on the installed
claude binary and live installs of git@holocron/gitea@holocron/kyberforge@
holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm
conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json
compiler deliberately strips skills/agents/commands keys, assuming the host
already auto-discovers those convention directories -- it has no model of
.apm/ being host-visible at all. Separately, apm's own bundle exporter
(apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`)
implements the correct .apm/ -> flat mapping, but only ever targeted
build/<name>-<version>/, a path nothing in marketplace.json's source: points
at.

scripts/sync-plugin-content.sh wraps that bundle exporter and copies its
agents/, skills/, commands/, instructions/, extensions/, and merged
hooks.json back into each plugin's own root as a second tracked
compiled-output category -- same governance status as
.claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/
subdirectories are excluded from the mirror (dev fixtures, not host-visible
runtime content; several hardcode a relative repo-root walk-up sized for the
.apm/-nested depth, which breaks when duplicated one level shallower).
Applied for real across all 6 plugins and verified two ways: `claude plugin
validate --strict` passes on every real plugin directory, and a live
`claude --plugin-dir <path> -p "list skills/agents"` behavioral test
confirms content is now actually discovered.

Also, from the same issue #90 review round:
- scripts/check-manifests.sh pointed at each plugin's root-level plugin.json
  (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a
  stale near-duplicate of .claude-plugin/plugin.json nothing else read or
  wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to
  validate .claude-plugin/plugin.json instead, and drops the pointer-field
  checks entirely (nothing to check -- those fields are correctly absent by
  design). Content-presence drift is now check-plugin-content-sync's job, a
  new pre-push hook wired in .pre-commit-config.yaml.

docs/adr/0017 records the root cause and decision in full, including two
rejected alternatives (patching plugin.json's path fields directly -- apm's
compiler strips them on every run; pointing marketplace.json at apm pack's
build/ output -- a version-suffixed non-source directory nothing can install
from without an extra build step). ADR-0015 and CONTEXT.md are updated to
point at it.

Refs: #90
2026-08-13 16:59:03 +00:00

124 lines
9.2 KiB
Markdown

---
name: git-remotes
description: >
Manage git remote repositories — add/remove/configure remotes, push/pull with safety checks,
handle fetch patterns and tracking branch updates, support multi-remote workflows.
Use when automating remote operations, pushing with force-push safety, fetching with pruning,
pulling with divergence resolution, or managing multi-remote tracking. Include indirect triggers:
any git operation that touches a remote, even if the user doesn't explicitly name the remote.
Do not use when working with local git history, commits, branches, or staging — use git-history
or git-branches instead.
metadata:
category: git-workflow
source_keys:
- git-scm-remote-docs
- git-scm-fetch-docs
- git-scm-push-docs
- git-scm-pull-docs
- context7-git-htmldocs
---
## Gotchas
- **Never force-push `main` or `master`, under any circumstances** — this is a hard refusal, not a `confirm: true` gate. If a force-push targets one of these branches, decline and explain why, regardless of how the request is confirmed.
- **Force-push to any other branch requires explicit confirmation** — never execute `git push --force` or `git push -f` without user/agent approval. Always ask or require `confirm: true` flag first.
- **`--force-with-lease` alone is not safe** — background processes (IDE plugins, cron jobs) that run `git fetch` silently defeat the protection. Always combine with `--force-if-includes` or use explicit SHA form `--force-with-lease=<ref>:<sha>`.
- **Prune doesn't touch tags by default** — `git fetch --prune` leaves orphaned tags. Use `git fetch --prune --prune-tags` or configure `fetch.pruneTags true` globally.
- **Pull with rebase rewrites history** — only safe for unpublished work. Rebasing already-pushed commits breaks everyone downstream. Check what's been pushed before rebasing.
- **`git remote show` requires network access** — use `-n` flag for cached data if working offline. `git remote -v` lists URLs without network queries.
- **Pull behavior defaults shift between Git versions** — older versions default to merge, newer versions to `--ff-only`. Always set `pull.ff only` explicitly for deterministic behavior.
## Operations
### Remote Management
Use these to configure which remotes you push to and pull from:
- **Add a remote**: `git remote add <name> <url>` or `git remote add -f <name> <url>` to fetch immediately
- **Remove a remote**: `git remote remove <name>` (deletes remote + all tracking refs + config)
- **Rename a remote**: `git remote rename <old> <new>`
- **Inspect remotes**: `git remote -v` (show URLs) or `git remote show <name>` (live tracking status, requires network)
- **Set-url separately for fetch vs. push**: `git remote set-url --push <name> <url>` changes only where pushes go — but fetch and push URLs must still reference the same repository. For genuine fetch-from-A / push-to-B workflows, use two separate named remotes instead; `--push` cannot do this. Full `set-url` variants (regex-targeted replace, `--add`, `--delete`): `references/remotes.md`.
- **Remove a stale URL**: `git remote set-url --delete <name> <regex>`
- **Inspect effective URLs**: `git remote get-url <name>` (shows URL after `insteadOf` rewrites) or `git remote get-url --push --all <name>` (all push URLs)
- **Track only one branch**: `git remote add -t <branch> <name> <url>` (repeatable), or suppress tag import entirely with `git remote add --no-tags <name> <url>`
- **Mirror a remote**: `git remote add --mirror=fetch <name> <url>` mirrors all refs locally (bare repos only); `--mirror=push` makes every push behave like `--mirror`
- **Prune stale tracking refs without fetching**: `git remote prune <name>` (add `--dry-run` to preview first)
- **Set the remote's default branch pointer**: `git remote set-head <name> -a` (auto-detect, requires a prior fetch), `git remote set-head <name> <branch>` (explicit), or `git remote set-head <name> -d` (delete `refs/remotes/<name>/HEAD`)
### Fetch Operations
Use these to update your tracking branches without touching your local branches:
- **Fetch from one remote**: `git fetch <remote>` — fetches all branches
- **Fetch one branch only**: `git fetch <remote> <branch>` — stores the result in `FETCH_HEAD`, not a tracking ref
- **Fetch from all remotes**: `git fetch --all` with optional `--prune` to clean up stale tracking refs
- **Prune properly**: Use `git fetch --all --prune --prune-tags` to clean both branches and tags
- **Configure auto-prune**: Set `git config --global fetch.prune true` to auto-prune on every fetch across all remotes (or `remote.<name>.prune` to scope it to one remote)
- **Shallow clones**: `--depth=<n>` to deepen or create a shallow clone, `--unshallow` to convert to full history, `--update-shallow` to allow the shallow boundary to move. Details and the default fetch refspec: `references/remotes.md`.
Fetch never modifies your local branches — it only updates remote-tracking branches (`refs/remotes/origin/*`).
### Push Operations
Use these to send your commits upstream. Default: safe push to same-named branch on the remote.
- **Basic push**: `git push <remote> <branch>` — pushes to same-named remote branch
- **Set upstream**: `git push -u <remote> <branch>` — push and configure this branch to track the remote
- **Multi-remote push**: `git push origin develop` and `git push staging develop` sequentially, or use `git remote set-url --add <name> <url>` to push to multiple remotes with one command
- **Force-push safety**: Always use `git push --force-with-lease --force-if-includes <remote> <branch>` over bare `--force`. Require explicit confirmation first — and never for `main`/`master` (see Gotchas). `--force-if-includes` is a no-op without `--force-with-lease`. If background tools (IDE, cron) auto-fetch and could poison the lease check, use a dedicated push-only remote instead — see `references/remotes.md`.
- **Server-side enforcement**: `receive.denyDeletes`, `receive.denyDeleteCurrent`, and `receive.denyNonFastForwards` are enforced on the remote regardless of local flags — a hardened server rejects the push even with `--force`.
- **Delete remote branch**: `git push <remote> --delete <branch>` (not `:<branch>` syntax; clearer and cleaner)
- **Push everything**: `git push --all` (all local branches) or `git push --tags` (all tags)
- **Push a single tag**: `git push origin <tag>`
- **Delete remote branches with no local counterpart**: `git push --prune origin 'refs/heads/*:refs/heads/*'`
- **Force only part of a multi-ref push**: prefix the one refspec that needs it with `+`, e.g. `git push origin +main develop` forces `main` while safe-pushing `develop`
Refspec syntax is `[+]<src>[:<dst>]`:
| Pattern | Meaning |
|---|---|
| `<branch>` | Push to same-named remote branch |
| `<src>:<dst>` | Push `<src>` local ref to `<dst>` remote ref |
| `+<src>:<dst>` | Force this refspec (non-fast-forward allowed) |
| `:<branch>` | Delete remote `<branch>` |
| `refs/heads/*:refs/heads/*` | Glob: push all matching branches |
| `^refs/heads/dev-*` | Negative: exclude matching refs |
| `tag <name>` | Sugar for `refs/tags/<name>:refs/tags/<name>` |
### Pull Operations
Use these to fetch and integrate remote changes. Default strategy: `--ff-only` (fail if diverged, forcing a conscious choice).
- **Pull with fast-forward only**: `git pull --ff-only` (recommended default — fails if you've diverged, forcing a rebase/merge decision)
- **Pull with rebase**: `git pull --rebase` (replays your unpublished commits on top; linear history, but rewrites SHAs — only safe for unpublished work)
- **Pull with merge**: `git pull --no-rebase` (three-way merge commit; preserves original commits, non-linear)
- **Pull with rebase, preserving merges**: `git pull --rebase=merges` (like `--rebase`, but keeps intentional local merge commits during replay)
- **Pull without integrating**: `git pull --squash` collapses incoming commits into staged changes without committing — you write the commit message
- **Set pull strategy globally**: `git config pull.ff only` (or `pull.rebase true`; respects branch-specific overrides via `branch.<name>.rebase`). Full precedence order (CLI flag > `pull.rebase` > `branch.<name>.rebase` > `branch.autoSetupRebase`): `references/remotes.md`.
- **Check before rebasing**: Always verify your commits haven't been pushed before using `--rebase`. Rebasing published commits breaks everyone downstream.
- **Merge strategy default**: Git 2.34+ defaults to the `ort` merge strategy (`recursive` is now just an alias for it). Strategy options like `-X ours`, `-X theirs`, `-X ignore-space-change` still pass through unchanged.
- **Submodules on pull**: `--recurse-submodules` only fetches submodules that are already checked out — newly added submodules are not initialized automatically. Use the `git-submodules` skill to initialize new ones.
If pull diverges and you haven't set a strategy, the operation fails — this is good, forces a conscious choice. Never auto-merge diverged branches without asking.
### Return Format (for agents)
Return structured output:
```json
{
"success": true,
"operation": "push",
"remote": "origin",
"branch": "main",
"output": "...",
"warnings": ["force-with-lease not confirmed"],
"recommendations": ["set pull.ff=only globally"]
}
```
On failure, include `error` field with root cause and recovery suggestion.