239 lines
10 KiB
Markdown
239 lines
10 KiB
Markdown
---
|
|
name: ckb-sync-changes
|
|
description: Reconcile this repo's git history with its origin remote — pull down remote commits, commit any local working-tree changes, resolve any conflicts with the user, then push. Use when the user asks to "sync changes", "sync with git", "sync with the remote", "sync with origin", "push and pull my changes", "reconcile git", or "sync the repo". This is a git-level operation, distinct from the content-level "Sync the wiki" / "Ingest" workflow in CLAUDE.md, which processes raw/inbox/ into structured wiki/ pages.
|
|
---
|
|
|
|
# Sync changes skill
|
|
|
|
## Purpose
|
|
|
|
Keep this repo's local working tree and its `origin` remote in sync in both
|
|
directions: pull remote commits down, commit local file changes, resolve any
|
|
conflicts with the user, then push. `wiki/`, `outputs/`, `workload/`, and
|
|
every other tracked path are treated as opaque files at this layer — this
|
|
skill only does git mechanics (stage/commit/fetch/merge/push). It never
|
|
invokes the Ingestion Workflow, never re-derives entities/graph/index/log
|
|
content, and never tries to semantically reconcile markdown or frontmatter —
|
|
a conflicted file is just text with conflict markers until the user says
|
|
otherwise.
|
|
|
|
Unlike the `ckb-export-okf` skill, this skill **does** commit and push on its
|
|
own once conflicts (if any) are resolved — that automation was explicitly
|
|
requested for this skill.
|
|
|
|
## Trigger phrases
|
|
|
|
Use this skill when the user says things like:
|
|
- "sync changes"
|
|
- "sync with git" / "sync with the remote" / "sync with origin"
|
|
- "push and pull my changes"
|
|
- "reconcile git"
|
|
- "sync the repo"
|
|
|
|
Do **not** use this skill for bare "Sync" or "Sync the wiki" — those trigger
|
|
the content-level Ingestion Workflow in `CLAUDE.md`/`AGENTS.md` §3 instead
|
|
(processing `raw/inbox/` into `wiki/`), which this skill has nothing to do
|
|
with.
|
|
|
|
## How to run this skill
|
|
|
|
### Step 1 — Pre-flight safety checks
|
|
|
|
Run:
|
|
```bash
|
|
git rev-parse --is-inside-work-tree
|
|
git status --porcelain=v1
|
|
```
|
|
|
|
Check for an in-progress merge/rebase/cherry-pick:
|
|
```bash
|
|
test -f .git/MERGE_HEAD && echo "MERGE IN PROGRESS"
|
|
test -f .git/REBASE_HEAD -o -d .git/rebase-merge -o -d .git/rebase-apply && echo "REBASE IN PROGRESS"
|
|
test -f .git/CHERRY_PICK_HEAD && echo "CHERRY-PICK IN PROGRESS"
|
|
```
|
|
If any of these report something in progress, **stop immediately**. Tell
|
|
the user a git operation is already underway and must be resolved or
|
|
aborted manually (`git merge --abort` / `git rebase --abort`) before running
|
|
this skill again. Do not try to clean it up yourself.
|
|
|
|
Scan the `git status --porcelain` output for paths that look like secrets:
|
|
`.env`, `.env.*`, anything containing `credentials`, `*.pem`, `*.key`,
|
|
`id_rsa*`, anything containing `secret` (case-insensitive). If any match,
|
|
ask the user via `AskUserQuestion` whether to include or skip each one
|
|
*before* staging anything in Step 3 — never silently commit or silently
|
|
drop a flagged file.
|
|
|
|
### Step 2 — Ensure `origin` is configured
|
|
|
|
```bash
|
|
git remote get-url origin
|
|
```
|
|
|
|
- **Succeeds** → go to Step 3.
|
|
- **Fails** (`fatal: No such remote 'origin'`, or `git remote` lists nothing
|
|
at all) → this repo has no remote to sync with yet. Ask the user directly
|
|
(a plain question is fine here — it's a URL to paste, not a choice between
|
|
options):
|
|
> "This repo has no `origin` remote configured. Paste the URL of the
|
|
> remote repository (e.g. `https://github.com/org/repo.git` or
|
|
> `git@github.com:org/repo.git`) and I'll add it as `origin`."
|
|
|
|
Once given:
|
|
```bash
|
|
git remote add origin <url>
|
|
```
|
|
Then continue to Step 3 with `origin` now configured. If `git remote add`
|
|
itself errors (malformed URL, etc.), report the raw error and stop — don't
|
|
guess at a corrected URL or retry with a modified one.
|
|
- **A differently-named remote already exists** (e.g. `upstream`) that looks
|
|
like it might be the intended remote — don't assume. Ask the user whether
|
|
that's the one to sync with (in which case, whether to alias/rename it to
|
|
`origin` since the rest of this skill assumes that name) or whether to add
|
|
a separate, new `origin`.
|
|
|
|
### Step 3 — Detect the first-run / unrelated-histories case
|
|
|
|
```bash
|
|
git fetch origin
|
|
git rev-parse HEAD # fails with "unknown revision" if local has no commits yet
|
|
git rev-parse origin/main # fails if the remote branch doesn't exist/is empty
|
|
```
|
|
|
|
- **Local `HEAD` exists** → go to Step 4, regardless of remote state.
|
|
- **Local `HEAD` doesn't exist, and `origin/main` doesn't exist or is empty**
|
|
→ this is a plain first publish, not a reconciliation. Proceed
|
|
automatically to Step 4 (it will just commit and push with nothing to
|
|
merge).
|
|
- **Local `HEAD` doesn't exist, but `origin/main` already has commits** →
|
|
**hard stop, every time this is detected.** Show the user:
|
|
```bash
|
|
git log origin/main --oneline | head -20
|
|
git status --porcelain # everything currently on local disk, untracked
|
|
```
|
|
Then ask via `AskUserQuestion` with exactly these three options:
|
|
- "Merge — bring in origin/main's history (`git merge --allow-unrelated-histories origin/main` after committing local content), then resolve any conflicts"
|
|
- "Remote wins — check out origin/main first (`git reset --hard` is not needed since there's no local commit yet; instead branch from origin/main), then reapply my local changes on top of it"
|
|
- "Stop — I'll sort this out myself"
|
|
|
|
Do **not** offer a fourth "discard remote, force local to become main"
|
|
option — that requires a force-push and is out of scope for this skill;
|
|
if the user wants that, tell them it needs to be done manually. Execute
|
|
only the option chosen, then continue to Step 4 with whichever git state
|
|
results.
|
|
|
|
### Step 4 — Steady-state flow
|
|
|
|
Order matters: **commit local changes first, then fetch/merge, then push.**
|
|
Committing first turns any overlap into an ordinary merge conflict (which
|
|
Step 5 already knows how to present), rather than a stash-pop conflict with
|
|
no commit boundary to fall back on.
|
|
|
|
```bash
|
|
# Commit local changes, if any (skip entirely if git status --porcelain is clean)
|
|
git add <path1> <path2> ... # explicit paths only — never `git add -A` or `git add .`
|
|
git commit -m "Sync: local changes as of <ISO timestamp>"
|
|
|
|
# Reconcile with remote
|
|
git fetch origin
|
|
git merge origin/main # merge, never rebase — rebase would rewrite local commits
|
|
# and require a force-push, which is out of scope here
|
|
```
|
|
|
|
Outcomes of the merge:
|
|
- `Already up to date.` or a clean auto-merge → continue below.
|
|
- `CONFLICT` → go to Step 5, then come back here once every conflict is
|
|
resolved and committed.
|
|
|
|
```bash
|
|
# Push
|
|
git push origin main
|
|
```
|
|
If the push is rejected as non-fast-forward (the remote moved again between
|
|
fetch and push), retry the fetch → merge → push cycle **exactly once**. If
|
|
it fails again, stop and report the raw error to the user — never force,
|
|
never retry more than once.
|
|
|
|
### Step 5 — Present each conflict to the user
|
|
|
|
```bash
|
|
git diff --name-only --diff-filter=U
|
|
```
|
|
For each path returned, read the full file. It may contain more than one
|
|
conflict block — handle each independently, leaving all surrounding
|
|
unconflicted content untouched. Each block looks like:
|
|
```
|
|
<<<<<<< HEAD
|
|
(local text)
|
|
=======
|
|
(remote text)
|
|
>>>>>>> origin/main
|
|
```
|
|
|
|
For each block:
|
|
- Call `AskUserQuestion` with the file path (and block number if the file
|
|
has more than one) as context, showing both the local and remote text in
|
|
full. Options: **"Keep local version"**, **"Keep remote version"**,
|
|
**"Provide merged text"**.
|
|
- If "Provide merged text" is chosen, ask the user to supply the
|
|
replacement text directly.
|
|
- Rewrite the block with the chosen/provided content and strip the three
|
|
marker lines for that block.
|
|
- If the conflict is on a binary file or the markers can't be cleanly
|
|
parsed, skip the "Provide merged text" option and instead resolve with
|
|
`git checkout --ours <path>` or `git checkout --theirs <path>` per the
|
|
user's local/remote choice.
|
|
|
|
Once every block in a file is resolved, `git add` that file immediately —
|
|
don't wait for every file to be done before staging the first one, so
|
|
progress is always visible in `git status`. Once every conflicted file is
|
|
staged, commit:
|
|
```bash
|
|
git commit -m "Merge origin/main (conflicts resolved with user input)"
|
|
```
|
|
Then return to Step 4's push.
|
|
|
|
### Step 6 — Report
|
|
|
|
End with a structured summary:
|
|
|
|
```
|
|
## Sync report
|
|
|
|
**Remote:** origin already configured (or "origin added: <url>")
|
|
**Pulled from origin/main:** <N> commit(s) — <oneline log, or "none, already up to date">
|
|
**Committed locally:** <N> file(s) — <paths>, commit <short-hash> (or "no local changes to commit")
|
|
**Conflicts encountered:** <N> (or "none")
|
|
- <path> (block N of M) — resolved: kept local / kept remote / custom merged text
|
|
**Secrets flagged:** <files and the user's choice, or "none">
|
|
**Push result:** origin/main now at <short-hash> (or the retry/failure detail if it didn't succeed)
|
|
```
|
|
|
|
If anything was committed under `wiki/` without a corresponding
|
|
`wiki/log.md` (or subdirectory `log.md`) update in the same diff, add a
|
|
one-line nudge to run Ingest/Lint afterward — this is a suggestion, not a
|
|
blocker.
|
|
|
|
## Edge cases
|
|
|
|
- **Nothing to do on either side** — report "already in sync," make no
|
|
commits, run no push.
|
|
- **Local changes only, remote unchanged** — commit, fast-forward push, no
|
|
merge/conflict step needed.
|
|
- **Remote changes only, local clean** — fast-forward merge, nothing to
|
|
commit, nothing to push; report "pulled N commits, nothing local to
|
|
push."
|
|
- **Push rejected twice in a row** — stop, report the raw git error, do not
|
|
force and do not retry a third time.
|
|
- **No `origin` remote configured** — handled proactively in Step 2 (ask
|
|
the user for the URL and add it), not treated as a failure.
|
|
- **User pastes an invalid or unreachable URL** — `git remote add` itself
|
|
usually still succeeds (it doesn't validate reachability); the failure
|
|
surfaces at the `git fetch origin` in Step 3. Report that raw error and
|
|
ask the user to confirm the URL rather than guessing a correction.
|
|
- **Fetch/push fails on auth/network once a remote is configured** — report
|
|
the error clearly. This skill does not manage git credentials.
|
|
|
|
---
|
|
|
|
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
|
|
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*
|