[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CVJ92KQJ0C7M97QHMMY2BQ":3,"skill-same:01M6CVJ92KQJ0C7M97QHMMY2BQ":91},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":46,"linkOnly":47,"markdownContent":48,"markdownBody":49,"frontMatter":50,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":56,"repositoryHtmlUrl":57,"repositoryHomepageUrl":6,"ownerHtmlUrl":58,"ownerBlogUrl":59,"ownerDescription":6,"updatedAt":60,"install":61,"resources":74},"01M6CVJ92KQJ0C7M97QHMMY2BQ","full-sync",null,"Full sync routine: pull all working repositories, let the agent decide what uncommitted work to commit and what to push, then bring the host up to date — apt update\u002Fupgrade, restart of opencode.service and the OpenCode Telegram bots (they upgrade themselves on restart), and a reboot when one is pending. Use when the user says \"full sync\", \"Voll-Sync\", \"repo sync\", \"morning sync\", \"Morgen-Sync\", \"sync everything\", \"Tagesstart\", \"pull all repos\" or \"commit and push everything\", or wants the daily sync routine. Do not use for a single repository — a plain `git pull` is enough there.","georgernstgraf\u002Fopencode-helpers","georgernstgraf","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F115418377?v=4","01M6CVHJ34JS0CF98EFFN0FZ76","Georg Graf SPG","01M6CVHJ7VV5VK6RXY62SR0NE2",1,"MIT","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Ffull-sync\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45],"sync","full sync","full","routine","repos","update","pull all repos","when","host","morning","reboot","opencode","work","telegram","commit","multi-repo","use","upgrade","commit and push","up","daily","decide","repo","apt","push","bots","pull","2026-11-10 10:12:30",false,"---\nname: full-sync\ndescription: >-\n  Full sync routine: pull all working repositories, let the agent decide what\n  uncommitted work to commit and what to push, then bring the host up to date —\n  apt update\u002Fupgrade, restart of opencode.service and the OpenCode Telegram bots\n  (they upgrade themselves on restart), and a reboot when one is pending. Use\n  when the user says \"full sync\", \"Voll-Sync\", \"repo sync\", \"morning sync\",\n  \"Morgen-Sync\", \"sync everything\", \"Tagesstart\", \"pull all repos\" or \"commit\n  and push everything\", or wants the daily sync routine. Do not use for a single\n  repository — a plain `git pull` is enough there.\nlicense: MIT\ncompatibility: opencode\nmetadata:\n  category: infrastructure\n  scope: multi-repo\n---\n\n# Full Sync\n\nSynchronize every configured working repository **and the host itself**. The\ngoal of a run: when it ends, every repository is on the state of its upstream —\nfast-forwarded or rebased where that is clean — and the host has current\npackages and current opencode services. The bundled script does the fetch, the\nclean catch-up and the host refresh; the **agent makes the commit and push\ndecisions**. The script never commits, never pushes.\n\n```\nfetch + catch up  ->  judge each repo  ->  commit \u002F push  ->  host steps  ->  report\n   (script)             (agent)             (agent)            (script)        (agent)\n```\n\n## The one rule\n\n**The script fetches and catches up; the agent decides commits and pushes.**\n`scripts\u002Ffull-sync.sh` fetches every remote, fast-forwards or rebases clean\ncheckouts onto their upstream, and reports dirty counts, ahead\u002Fbehind, branches\nand upstreams. It exits non-zero if any repository failed — a conflict, a failed\nfetch, or a failed pull. What to commit and what to push stays an agent decision.\n\n**The host steps are a second invocation, and they come last.**\n`scripts\u002Ffull-sync.sh --host-steps` refreshes the host (apt, services, reboot).\nIt runs only after Phase B\u002FC are done — a service restart ends the calling\nopencode session, so a restart inside the sync run would kill the session before\nthe commit\u002Fpush judgment could happen. It refuses to run when the most recent\nsync report was not clean (`failures != 0`).\n\n## No manual opencode upgrade — ever\n\nThere is no `opencode upgrade` step in this skill, and none is needed:\n\n- `opencode.service` runs `opencode upgrade --method curl` as `ExecStartPre`\n  on every start, so a restart picks up the newest version.\n- The OpenCode Telegram bots (`oc-tg-bot-*`) run `npx @grinev\u002Fopencode-telegram-bot@latest`,\n  so a restart picks up the newest bot version.\n\nA manual upgrade call before a restart is redundant. If opencode seems stale,\nthe answer is a service restart, not an upgrade call.\n\n## Config\n\n`config.json` next to this file is the versioned source of truth:\n\n| Key | Meaning |\n|---|---|\n| `roots[]` | `{type, path, max_depth}` — `git` roots are scanned for `.git`, `svn` roots for `.svn`. `~` is expanded. |\n| `push.allow_owners[]` | Remote owners (second-to-last path segment) that may be pushed to. |\n| `push.deny_paths[]` | Roots that are never pushed, regardless of owner. |\n| `push.require_upstream` | When true, a repo with no upstream is never pushed. |\n| `commit.never_commit_globs[]` | Hard secrets that must never be staged. |\n| `commit.max_review_files` | Above this many changed files, do not commit blindly. |\n| `fetch.all_remotes` | Fetch every remote of a repo, not just `origin`. |\n| `fetch.remove_dead_remotes` | When true (default), remove a **secondary** remote whose repository no longer exists. Never removes the upstream remote or a repo's only remote. |\n| `pull.enabled` | When true (default), clean behind\u002Fdiverged checkouts are caught up automatically. |\n| `pull.on_diverged` | `rebase` (default) replays the local-only commits onto the upstream; any other value leaves a diverged repo untouched. |\n| `pull.on_conflict` | `abort-and-report` (default) aborts a conflicting rebase and leaves the repo unchanged. |\n| `host_steps.system_upgrade` | `apt update` + `apt upgrade` (non-interactive, passwordless sudo required; `false` on non-Debian hosts would only ERROR). |\n| `host_steps.restart_services` | Restart the user services from `host_steps.services` — skipped when a reboot is pending anyway. |\n| `host_steps.services[]` | Exact unit names or globs (`oc-tg-bot-*`). A unit that does not exist on the current host is a `WARN`, not an error — the list is shared across hosts. |\n| `host_steps.reboot_if_required` | When true and `\u002Fvar\u002Frun\u002Freboot-required` exists, reboot at the very end. When a reboot is pending but this is false, a `WARN` is printed. |\n\n`max_depth` is the depth **of the VCS directory**, not the repository: for\n`~\u002Frepos\u002F\u003Cowner>\u002F\u003Crepo>\u002F.git` that is `3`, for `~\u002Fsvn\u002F\u003Cwc>\u002F.svn` it is `2`.\n\n## Phase A — Fetch & catch up\n\n```sh\n\"$HOME\u002F.config\u002Fopencode\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh\"\n```\n\nResolves through the symlink; `~\u002Frepos\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh`\nworks too. Full output is also written to\n`~\u002F.local\u002Fstate\u002Ffull-sync\u002Flatest.log` — read it if the in-session output was cut\nshort by a restart.\n\nFor each git repository the script:\n\n1. fetches **every** remote (not just `origin`), so `fork`\u002F`upstream` are current\n   too — with `GIT_TERMINAL_PROMPT=0`, a credential prompt can never block the run.\n   A failed fetch of the upstream's remote is an `ERROR`; a failed fetch of a\n   **secondary** remote is a `WARN`, and when its repository no longer exists\n   (\"Repository not found\") the stale config entry is removed\n   (`fetch.remove_dead_remotes`) — never the upstream remote, never a repo's only\n   remote;\n2. measures `ahead`\u002F`behind` against the branch's upstream **after** that fetch;\n3. catches the checked-out branch up **cleanly**:\n   - clean and only behind → `git merge --ff-only \u003Cupstream>` → `pull=ff(n)`\n   - clean and diverged → `git rebase \u003Cupstream>`, replaying the local-only\n     commits on top → `pull=rebase(n)`\n   - dirty → left untouched, `pull=skipped(dirty)` (the agent commits first)\n   - no upstream \u002F pull disabled → `pull=skipped(no-upstream)` \u002F `pull=disabled`\n   - rebase conflict → `git rebase --abort`, repo unchanged, `pull=conflict` + `ERROR`\n\nSVN working copies get `svn up` — the same catch-up.\n\nAn `ERROR:` line (failed fetch of the **upstream's** remote, a rebase conflict,\na failed pull) blocks **that repository only** — no commit, no push for it. A\n**secondary** remote that no longer resolves (e.g. a deleted fork) is only a\n`WARN:` and never stops the catch-up, because it does not affect the tracked\nbranch; when it is not the repo's only remote, its stale config entry is removed\n(`pruned=`). The other repositories in the run are unaffected and must still be\nsynced; one unreachable remote must not stall the other 74.\n\nTwo errors are **not** per-repository and do stop everything:\n\n- exit code **2** — the script could not start (unparsable config, missing tool,\n  bad usage). Fix the configuration before doing anything else.\n- a `WARN: … root does not exist` line — that host simply has no such root;\n  the remaining roots still run.\n\nThe report format is one `## \u003Ctype> \u003Cpath>` block per repository, then\n`key=value` lines plus, if anything changed, a `--- status ---` block:\n\n| Key | Meaning |\n|---|---|\n| `owner`, `branch`, `upstream`, `remote` | identity of the checkout |\n| `ahead`, `behind` | counts **after** the catch-up above |\n| `diverged` | `true` if the branch was ahead **and** behind when collected |\n| `dirty` | number of changed paths (the `--- status ---` block lists them) |\n| `fetched` | remotes fetched this run, comma-separated |\n| `pruned` | dead secondary remotes removed this run, comma-separated |\n| `pull` | `uptodate` \\| `ff(n)` \\| `rebase(n)` \\| `skipped(\u003Creason>)` \\| `disabled` \\| `conflict` \\| `failed` |\n\n**A run is done only when nothing is left behind.** After Phase A every repo\nshould show `behind=0`. A repo that still shows `behind>0` could not be caught up\ncleanly — it was dirty or conflicted. Handle it in Phase B; if the report is\nunclear about what a repo needs, **ask the user instead of guessing.**\n\n## Phase B — Commit judgment\n\nOnly for repositories with `dirty > 0`. Read the status block, then decide per\nrepository.\n\n**Commit** when the change is coherent, self-explanatory, and recognizable as\nfinished work:\n\n- one logical unit — all changed files serve a single purpose\n- recognizable as completed agent or author work: a new dated lesson folder, a\n  documentation update, a rename that replaces an older folder\n- no half-finished debugging, no stray scratch files, no secrets\n\nWrite a commit message in the repository's existing style. Do not invent issue\nnumbers. If the repository tracks issues, link the real one.\n\n**Leave it, and say why** in the report, when:\n\n- **another process is writing into the repository right now** — see below\n- the change mixes unrelated concerns and cannot be split without judgment\n- more than `commit.max_review_files` files changed — report, do not bulk-add\n- a file matches `commit.never_commit_globs` (secrets) — never stage those\n- the content is genuinely ambiguous: unfinished, or unclear what it is for\n\n### Check for a concurrent writer first\n\nThe single most damaging commit this skill can make is one that snapshots work\nanother agent is halfway through. Before committing a dirty repository, check\nwhether it is still being written to:\n\n```sh\nfind \u003Crepo> -newermt '-30 minutes' -not -path '*\u002F.git\u002F*' -printf '%TH:%TM %p\\n'\nps aux | grep -E 'opencode (attach|serve)|claude|codex' | grep -v grep\n```\n\nFresh mtimes are normal for a repository you just worked on yourself, so compare\nagainst your own timeline rather than treating any recent file as suspect. But\nif a **second** opencode\u002Fagent session is running against the same tree, the\nchanged files are work in progress: leave the repository alone and name it in the\nreport. A half-written lesson that is missing its `praesentation.html` is worse\nin history than uncommitted — it is a broken artifact everyone will trust.\n\nThis is a real occurrence, not a hypothetical: a concurrent `opencode attach`\nwas generating GRG-PMM lessons while the sync ran, and the incomplete\n`KM7-02-chi-quadrat-test\u002F` had to be skipped.\n\nContent types that are **not** a reason to hold back, when the repository\nalready tracks them: PDFs, student submissions, CatchLog files, generated output.\n`matura-diplom-koll` tracks 110 PDFs and commits them deliberately —\nmatching the repository's existing practice is the correct default.\n\nAfter committing, a repo that Phase A skipped with `pull=skipped(dirty)` may\nstill be behind its upstream. Catch it up now — re-run the script, or run\n`git pull --ff-only` \u002F `git pull --rebase` in that repo — so the run ends with\n`behind=0` before you move on.\n\n## Phase C — Push judgment\n\nPush a repository only when **all** of these hold:\n\n1. its `owner` is in `push.allow_owners`\n2. its path is not under `push.deny_paths`\n3. `require_upstream` is satisfied — a repo without an upstream has nowhere to go\n4. the `fork-policy` skill does not apply to it (forks stay on feature branches)\n5. it is green: the repository's tests and lint pass, if it has any\n\nA `fetch` error in Phase A blocks **that repository**, not the run.\n\n`deny_paths` exists for two shapes of repository: a **pull-only foreign\nupstream** (`mattpocock\u002Fskills` — pushing there would corrupt a checkout the\nuser only mirrors) and **local-only clones** (`no-remote\u002F`, `non-git\u002F`).\n\nNon-default branches are not a reason to hold back. If the owner is allowed and\nthe branch has an upstream, the branch is the user's own work and is pushed.\n\n## Phase D — Host steps (always, and last)\n\nAfter Phase B\u002FC — never before, never instead — run:\n\n```sh\n\"$HOME\u002F.config\u002Fopencode\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh\" --host-steps\n```\n\nThe host steps are a **fixed part of every full-sync run** (`host_steps.*` are\n`true` in the shipped config), but they live in their own invocation so the\ncommit\u002Fpush judgment survives the service restart. The script refuses to run\nwhen the most recent sync report was not clean, so a broken sync can never be\npapered over by a host refresh.\n\nIn order:\n\n1. **System update** — `sudo apt-get update` && `sudo apt-get upgrade -y`\n   (non-interactive, `--force-confold` for conffiles). Requires passwordless\n   sudo; without it the step is an `ERROR`, not a silent skip.\n2. **Service restart** — every unit in `host_steps.services` (globs allowed;\n   `opencode.service`, `oc-tg-bot-*` on gregor). A unit missing on the current\n   host is a `WARN`. **Restarting `opencode.service` ends the calling opencode\n   session** — the client reconnects. When a reboot is pending anyway, the\n   restarts are skipped: the units come back fresh after the reboot.\n3. **Reboot** — when `\u002Fvar\u002Frun\u002Freboot-required` exists (freshly set by apt or\n   left over), the host reboots at the very end. This ends the run, the session\n   and the machine; whatever still needs saying must be in the final report\n   text or the log **before** this point.\n\nReport lines to carry into Phase E: `system-upgrade=ok`, `reboot-required=true|false`,\nplus one `==> restarting \u003Cunit>` line per restarted service.\n\n## Phase E — Report\n\nAlways close with a table, then a \"left alone\" section listing every\nrepository you deliberately did not touch, with the reason. Silent omission\nmakes a partial sync look like a complete one. **Compose and send the report\nBEFORE the `--host-steps` invocation** — a session that dies with the service\nrestart takes an unsent report with it; the log path is the fallback.\n\n```\n| Repository | Action | Commit | Push |\n|---|---|---|---|\n| georgernstgraf\u002FGRG-WMC | committed | 1a2b3c4 | yes |\n| georgernstgraf\u002Fmatura-diplom-koll | committed | 5d6e7f8 | yes |\n| someorg\u002Frepo | left alone | — | no — 40+ files changed, needs review |\n```\n\nName the log path so the user can read the raw report afterwards, and state\nwhat the host steps did (packages, services, reboot).\n\n## Done when\n\n- [ ] Phase A ran and every repository's `pull`\u002F`behind` state was read.\n- [ ] Every dirty repository was either committed or named in the report with\n      its reason; no secrets match and no `max_review_files` overflow was staged.\n- [ ] No commit snapshotted a repository another agent was still writing.\n- [ ] Every committed repository ended at `behind=0`, or its leftover state is\n      in the report.\n- [ ] Pushes satisfied all five Phase C conditions; forks stayed on their\n      branches.\n- [ ] The report table plus the \"left alone\" section were composed before the\n      `--host-steps` invocation.\n- [ ] `--host-steps` ran last, and the report states its outcome (packages,\n      services, reboot) and the log path.\n\n## Self-test\n\n`scripts\u002Ftest-full-sync.sh` builds throwaway bare remotes and clones in a temp\ndir and checks every pull decision (uptodate, behind → ff, diverged → rebase,\ndirty → skip, no upstream, pull disabled). Run it after any change to the fetch\nor pull logic; it must stay green and it never touches a real repository.\nHermetic `unittest` coverage lives in `tests\u002Ftest_full_sync.py` — it also pins\nthe `--host-steps` gate (refuses after a failed sync) and the shipped config\nshape.\n\n## Pitfalls\n\n- **The script fetches and catches up, but must stay commit- and push-free.**\n  A fast-forward\u002Frebase of a clean checkout is mechanical; deciding what to\n  commit and what to push is not — that judgment stays with the agent.\n- **Never log an unredacted remote URL.** The script masks\n  `https:\u002F\u002Fuser:token@host` and `ghp_`\u002F`gho_` tokens before anything reaches the\n  log. Do not print remotes yourself outside that path.\n- **Credentials belong in the credential store, not in the URL.** `git@github.com:`\n  is the house style for 20 of 20 remotes; `https:\u002F\u002Fuser:token@` leaves the token\n  in `.git\u002Fconfig`.\n- **`~\u002Fbin` is not on the agent's PATH** (only `.bash_aliases` adds it in\n  interactive shells), so absolute paths are required for anything in there.\n- **Do not reintroduce `~\u002Fbin\u002Fsvnkack.sh`.** It commits with an empty message\n  (`svn commit -mm`), never checks errors, and 3 of its 5 paths no longer exist.\n  SVN commits are an agent decision, same as Git.\n- **Host steps are a separate invocation, after Phase B\u002FC.** `--host-steps`\n  restarts user services and ends the calling opencode session; a restart inside\n  the sync run would kill the session before the commit\u002Fpush judgment. The gate\n  (refuses when the last sync report was not clean) is a backstop, not a license\n  to skip the judgment.\n- **No manual `opencode upgrade`.** `opencode.service` upgrades itself via\n  `ExecStartPre`, the Telegram bots run `npx ...@latest` — a restart alone picks\n  up the newest versions. A manual upgrade call is redundant and hides real\n  restart problems.\n- **A pending reboot skips the service restarts on purpose** — the units come\n  back fresh after the reboot; restarting them first would be wasted work and a\n  second surprise restart.\n","# Full Sync\n\nSynchronize every configured working repository **and the host itself**. The\ngoal of a run: when it ends, every repository is on the state of its upstream —\nfast-forwarded or rebased where that is clean — and the host has current\npackages and current opencode services. The bundled script does the fetch, the\nclean catch-up and the host refresh; the **agent makes the commit and push\ndecisions**. The script never commits, never pushes.\n\n```\nfetch + catch up  ->  judge each repo  ->  commit \u002F push  ->  host steps  ->  report\n   (script)             (agent)             (agent)            (script)        (agent)\n```\n\n## The one rule\n\n**The script fetches and catches up; the agent decides commits and pushes.**\n`scripts\u002Ffull-sync.sh` fetches every remote, fast-forwards or rebases clean\ncheckouts onto their upstream, and reports dirty counts, ahead\u002Fbehind, branches\nand upstreams. It exits non-zero if any repository failed — a conflict, a failed\nfetch, or a failed pull. What to commit and what to push stays an agent decision.\n\n**The host steps are a second invocation, and they come last.**\n`scripts\u002Ffull-sync.sh --host-steps` refreshes the host (apt, services, reboot).\nIt runs only after Phase B\u002FC are done — a service restart ends the calling\nopencode session, so a restart inside the sync run would kill the session before\nthe commit\u002Fpush judgment could happen. It refuses to run when the most recent\nsync report was not clean (`failures != 0`).\n\n## No manual opencode upgrade — ever\n\nThere is no `opencode upgrade` step in this skill, and none is needed:\n\n- `opencode.service` runs `opencode upgrade --method curl` as `ExecStartPre`\n  on every start, so a restart picks up the newest version.\n- The OpenCode Telegram bots (`oc-tg-bot-*`) run `npx @grinev\u002Fopencode-telegram-bot@latest`,\n  so a restart picks up the newest bot version.\n\nA manual upgrade call before a restart is redundant. If opencode seems stale,\nthe answer is a service restart, not an upgrade call.\n\n## Config\n\n`config.json` next to this file is the versioned source of truth:\n\n| Key | Meaning |\n|---|---|\n| `roots[]` | `{type, path, max_depth}` — `git` roots are scanned for `.git`, `svn` roots for `.svn`. `~` is expanded. |\n| `push.allow_owners[]` | Remote owners (second-to-last path segment) that may be pushed to. |\n| `push.deny_paths[]` | Roots that are never pushed, regardless of owner. |\n| `push.require_upstream` | When true, a repo with no upstream is never pushed. |\n| `commit.never_commit_globs[]` | Hard secrets that must never be staged. |\n| `commit.max_review_files` | Above this many changed files, do not commit blindly. |\n| `fetch.all_remotes` | Fetch every remote of a repo, not just `origin`. |\n| `fetch.remove_dead_remotes` | When true (default), remove a **secondary** remote whose repository no longer exists. Never removes the upstream remote or a repo's only remote. |\n| `pull.enabled` | When true (default), clean behind\u002Fdiverged checkouts are caught up automatically. |\n| `pull.on_diverged` | `rebase` (default) replays the local-only commits onto the upstream; any other value leaves a diverged repo untouched. |\n| `pull.on_conflict` | `abort-and-report` (default) aborts a conflicting rebase and leaves the repo unchanged. |\n| `host_steps.system_upgrade` | `apt update` + `apt upgrade` (non-interactive, passwordless sudo required; `false` on non-Debian hosts would only ERROR). |\n| `host_steps.restart_services` | Restart the user services from `host_steps.services` — skipped when a reboot is pending anyway. |\n| `host_steps.services[]` | Exact unit names or globs (`oc-tg-bot-*`). A unit that does not exist on the current host is a `WARN`, not an error — the list is shared across hosts. |\n| `host_steps.reboot_if_required` | When true and `\u002Fvar\u002Frun\u002Freboot-required` exists, reboot at the very end. When a reboot is pending but this is false, a `WARN` is printed. |\n\n`max_depth` is the depth **of the VCS directory**, not the repository: for\n`~\u002Frepos\u002F\u003Cowner>\u002F\u003Crepo>\u002F.git` that is `3`, for `~\u002Fsvn\u002F\u003Cwc>\u002F.svn` it is `2`.\n\n## Phase A — Fetch & catch up\n\n```sh\n\"$HOME\u002F.config\u002Fopencode\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh\"\n```\n\nResolves through the symlink; `~\u002Frepos\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh`\nworks too. Full output is also written to\n`~\u002F.local\u002Fstate\u002Ffull-sync\u002Flatest.log` — read it if the in-session output was cut\nshort by a restart.\n\nFor each git repository the script:\n\n1. fetches **every** remote (not just `origin`), so `fork`\u002F`upstream` are current\n   too — with `GIT_TERMINAL_PROMPT=0`, a credential prompt can never block the run.\n   A failed fetch of the upstream's remote is an `ERROR`; a failed fetch of a\n   **secondary** remote is a `WARN`, and when its repository no longer exists\n   (\"Repository not found\") the stale config entry is removed\n   (`fetch.remove_dead_remotes`) — never the upstream remote, never a repo's only\n   remote;\n2. measures `ahead`\u002F`behind` against the branch's upstream **after** that fetch;\n3. catches the checked-out branch up **cleanly**:\n   - clean and only behind → `git merge --ff-only \u003Cupstream>` → `pull=ff(n)`\n   - clean and diverged → `git rebase \u003Cupstream>`, replaying the local-only\n     commits on top → `pull=rebase(n)`\n   - dirty → left untouched, `pull=skipped(dirty)` (the agent commits first)\n   - no upstream \u002F pull disabled → `pull=skipped(no-upstream)` \u002F `pull=disabled`\n   - rebase conflict → `git rebase --abort`, repo unchanged, `pull=conflict` + `ERROR`\n\nSVN working copies get `svn up` — the same catch-up.\n\nAn `ERROR:` line (failed fetch of the **upstream's** remote, a rebase conflict,\na failed pull) blocks **that repository only** — no commit, no push for it. A\n**secondary** remote that no longer resolves (e.g. a deleted fork) is only a\n`WARN:` and never stops the catch-up, because it does not affect the tracked\nbranch; when it is not the repo's only remote, its stale config entry is removed\n(`pruned=`). The other repositories in the run are unaffected and must still be\nsynced; one unreachable remote must not stall the other 74.\n\nTwo errors are **not** per-repository and do stop everything:\n\n- exit code **2** — the script could not start (unparsable config, missing tool,\n  bad usage). Fix the configuration before doing anything else.\n- a `WARN: … root does not exist` line — that host simply has no such root;\n  the remaining roots still run.\n\nThe report format is one `## \u003Ctype> \u003Cpath>` block per repository, then\n`key=value` lines plus, if anything changed, a `--- status ---` block:\n\n| Key | Meaning |\n|---|---|\n| `owner`, `branch`, `upstream`, `remote` | identity of the checkout |\n| `ahead`, `behind` | counts **after** the catch-up above |\n| `diverged` | `true` if the branch was ahead **and** behind when collected |\n| `dirty` | number of changed paths (the `--- status ---` block lists them) |\n| `fetched` | remotes fetched this run, comma-separated |\n| `pruned` | dead secondary remotes removed this run, comma-separated |\n| `pull` | `uptodate` \\| `ff(n)` \\| `rebase(n)` \\| `skipped(\u003Creason>)` \\| `disabled` \\| `conflict` \\| `failed` |\n\n**A run is done only when nothing is left behind.** After Phase A every repo\nshould show `behind=0`. A repo that still shows `behind>0` could not be caught up\ncleanly — it was dirty or conflicted. Handle it in Phase B; if the report is\nunclear about what a repo needs, **ask the user instead of guessing.**\n\n## Phase B — Commit judgment\n\nOnly for repositories with `dirty > 0`. Read the status block, then decide per\nrepository.\n\n**Commit** when the change is coherent, self-explanatory, and recognizable as\nfinished work:\n\n- one logical unit — all changed files serve a single purpose\n- recognizable as completed agent or author work: a new dated lesson folder, a\n  documentation update, a rename that replaces an older folder\n- no half-finished debugging, no stray scratch files, no secrets\n\nWrite a commit message in the repository's existing style. Do not invent issue\nnumbers. If the repository tracks issues, link the real one.\n\n**Leave it, and say why** in the report, when:\n\n- **another process is writing into the repository right now** — see below\n- the change mixes unrelated concerns and cannot be split without judgment\n- more than `commit.max_review_files` files changed — report, do not bulk-add\n- a file matches `commit.never_commit_globs` (secrets) — never stage those\n- the content is genuinely ambiguous: unfinished, or unclear what it is for\n\n### Check for a concurrent writer first\n\nThe single most damaging commit this skill can make is one that snapshots work\nanother agent is halfway through. Before committing a dirty repository, check\nwhether it is still being written to:\n\n```sh\nfind \u003Crepo> -newermt '-30 minutes' -not -path '*\u002F.git\u002F*' -printf '%TH:%TM %p\\n'\nps aux | grep -E 'opencode (attach|serve)|claude|codex' | grep -v grep\n```\n\nFresh mtimes are normal for a repository you just worked on yourself, so compare\nagainst your own timeline rather than treating any recent file as suspect. But\nif a **second** opencode\u002Fagent session is running against the same tree, the\nchanged files are work in progress: leave the repository alone and name it in the\nreport. A half-written lesson that is missing its `praesentation.html` is worse\nin history than uncommitted — it is a broken artifact everyone will trust.\n\nThis is a real occurrence, not a hypothetical: a concurrent `opencode attach`\nwas generating GRG-PMM lessons while the sync ran, and the incomplete\n`KM7-02-chi-quadrat-test\u002F` had to be skipped.\n\nContent types that are **not** a reason to hold back, when the repository\nalready tracks them: PDFs, student submissions, CatchLog files, generated output.\n`matura-diplom-koll` tracks 110 PDFs and commits them deliberately —\nmatching the repository's existing practice is the correct default.\n\nAfter committing, a repo that Phase A skipped with `pull=skipped(dirty)` may\nstill be behind its upstream. Catch it up now — re-run the script, or run\n`git pull --ff-only` \u002F `git pull --rebase` in that repo — so the run ends with\n`behind=0` before you move on.\n\n## Phase C — Push judgment\n\nPush a repository only when **all** of these hold:\n\n1. its `owner` is in `push.allow_owners`\n2. its path is not under `push.deny_paths`\n3. `require_upstream` is satisfied — a repo without an upstream has nowhere to go\n4. the `fork-policy` skill does not apply to it (forks stay on feature branches)\n5. it is green: the repository's tests and lint pass, if it has any\n\nA `fetch` error in Phase A blocks **that repository**, not the run.\n\n`deny_paths` exists for two shapes of repository: a **pull-only foreign\nupstream** (`mattpocock\u002Fskills` — pushing there would corrupt a checkout the\nuser only mirrors) and **local-only clones** (`no-remote\u002F`, `non-git\u002F`).\n\nNon-default branches are not a reason to hold back. If the owner is allowed and\nthe branch has an upstream, the branch is the user's own work and is pushed.\n\n## Phase D — Host steps (always, and last)\n\nAfter Phase B\u002FC — never before, never instead — run:\n\n```sh\n\"$HOME\u002F.config\u002Fopencode\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh\" --host-steps\n```\n\nThe host steps are a **fixed part of every full-sync run** (`host_steps.*` are\n`true` in the shipped config), but they live in their own invocation so the\ncommit\u002Fpush judgment survives the service restart. The script refuses to run\nwhen the most recent sync report was not clean, so a broken sync can never be\npapered over by a host refresh.\n\nIn order:\n\n1. **System update** — `sudo apt-get update` && `sudo apt-get upgrade -y`\n   (non-interactive, `--force-confold` for conffiles). Requires passwordless\n   sudo; without it the step is an `ERROR`, not a silent skip.\n2. **Service restart** — every unit in `host_steps.services` (globs allowed;\n   `opencode.service`, `oc-tg-bot-*` on gregor). A unit missing on the current\n   host is a `WARN`. **Restarting `opencode.service` ends the calling opencode\n   session** — the client reconnects. When a reboot is pending anyway, the\n   restarts are skipped: the units come back fresh after the reboot.\n3. **Reboot** — when `\u002Fvar\u002Frun\u002Freboot-required` exists (freshly set by apt or\n   left over), the host reboots at the very end. This ends the run, the session\n   and the machine; whatever still needs saying must be in the final report\n   text or the log **before** this point.\n\nReport lines to carry into Phase E: `system-upgrade=ok`, `reboot-required=true|false`,\nplus one `==> restarting \u003Cunit>` line per restarted service.\n\n## Phase E — Report\n\nAlways close with a table, then a \"left alone\" section listing every\nrepository you deliberately did not touch, with the reason. Silent omission\nmakes a partial sync look like a complete one. **Compose and send the report\nBEFORE the `--host-steps` invocation** — a session that dies with the service\nrestart takes an unsent report with it; the log path is the fallback.\n\n```\n| Repository | Action | Commit | Push |\n|---|---|---|---|\n| georgernstgraf\u002FGRG-WMC | committed | 1a2b3c4 | yes |\n| georgernstgraf\u002Fmatura-diplom-koll | committed | 5d6e7f8 | yes |\n| someorg\u002Frepo | left alone | — | no — 40+ files changed, needs review |\n```\n\nName the log path so the user can read the raw report afterwards, and state\nwhat the host steps did (packages, services, reboot).\n\n## Done when\n\n- [ ] Phase A ran and every repository's `pull`\u002F`behind` state was read.\n- [ ] Every dirty repository was either committed or named in the report with\n      its reason; no secrets match and no `max_review_files` overflow was staged.\n- [ ] No commit snapshotted a repository another agent was still writing.\n- [ ] Every committed repository ended at `behind=0`, or its leftover state is\n      in the report.\n- [ ] Pushes satisfied all five Phase C conditions; forks stayed on their\n      branches.\n- [ ] The report table plus the \"left alone\" section were composed before the\n      `--host-steps` invocation.\n- [ ] `--host-steps` ran last, and the report states its outcome (packages,\n      services, reboot) and the log path.\n\n## Self-test\n\n`scripts\u002Ftest-full-sync.sh` builds throwaway bare remotes and clones in a temp\ndir and checks every pull decision (uptodate, behind → ff, diverged → rebase,\ndirty → skip, no upstream, pull disabled). Run it after any change to the fetch\nor pull logic; it must stay green and it never touches a real repository.\nHermetic `unittest` coverage lives in `tests\u002Ftest_full_sync.py` — it also pins\nthe `--host-steps` gate (refuses after a failed sync) and the shipped config\nshape.\n\n## Pitfalls\n\n- **The script fetches and catches up, but must stay commit- and push-free.**\n  A fast-forward\u002Frebase of a clean checkout is mechanical; deciding what to\n  commit and what to push is not — that judgment stays with the agent.\n- **Never log an unredacted remote URL.** The script masks\n  `https:\u002F\u002Fuser:token@host` and `ghp_`\u002F`gho_` tokens before anything reaches the\n  log. Do not print remotes yourself outside that path.\n- **Credentials belong in the credential store, not in the URL.** `git@github.com:`\n  is the house style for 20 of 20 remotes; `https:\u002F\u002Fuser:token@` leaves the token\n  in `.git\u002Fconfig`.\n- **`~\u002Fbin` is not on the agent's PATH** (only `.bash_aliases` adds it in\n  interactive shells), so absolute paths are required for anything in there.\n- **Do not reintroduce `~\u002Fbin\u002Fsvnkack.sh`.** It commits with an empty message\n  (`svn commit -mm`), never checks errors, and 3 of its 5 paths no longer exist.\n  SVN commits are an agent decision, same as Git.\n- **Host steps are a separate invocation, after Phase B\u002FC.** `--host-steps`\n  restarts user services and ends the calling opencode session; a restart inside\n  the sync run would kill the session before the commit\u002Fpush judgment. The gate\n  (refuses when the last sync report was not clean) is a backstop, not a license\n  to skip the judgment.\n- **No manual `opencode upgrade`.** `opencode.service` upgrades itself via\n  `ExecStartPre`, the Telegram bots run `npx ...@latest` — a restart alone picks\n  up the newest versions. A manual upgrade call is redundant and hides real\n  restart problems.\n- **A pending reboot skips the service restarts on purpose** — the units come\n  back fresh after the reboot; restarting them first would be wasted work and a\n  second surprise restart.\n",{"name":5,"license":15,"metadata":51,"description":7,"compatibility":30},{"scope":34,"category":52},"infrastructure","main","adef05bd2193559aced970b8591fc8182a916301","skills\u002Ffull-sync",0,"https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf","https:\u002F\u002Fwww.spengergasse.at\u002F","2026-11-11 16:51:38",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add georgernstgraf\u002Fopencode-helpers --skill full-sync",{"tool":67,"command":68},"bunx","bunx skills add georgernstgraf\u002Fopencode-helpers --skill full-sync",{"tool":70,"command":71},"pnpm","pnpm dlx skills add georgernstgraf\u002Fopencode-helpers --skill full-sync","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Ffull-sync\u002FSKILL.md",true,[75,81,87],{"path":76,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":79,"sourceHtmlUrl":80},"config.json","REFERENCE","参考文档",920,"https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Ffull-sync\u002Fconfig.json",{"path":82,"resourceType":83,"resourceTypeDesc":84,"sizeBytes":85,"sourceHtmlUrl":86},"scripts\u002Ffull-sync.sh","SCRIPT","脚本",19315,"https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ffull-sync.sh",{"path":88,"resourceType":83,"resourceTypeDesc":84,"sizeBytes":89,"sourceHtmlUrl":90},"scripts\u002Ftest-full-sync.sh",5836,"https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Ffull-sync\u002Fscripts\u002Ftest-full-sync.sh",[92,110,132,148,163,175],{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":103,"publishedAt":46},"01M6CVKZ762FN4SW18V26P65TD","teach","Teach the user a new skill or concept, within this workspace.","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Fteach\u002FSKILL.md",[98],{"categoryNo":99,"categoryName":100,"taxonomyType":101,"taxonomyTypeDesc":102},"01JBSEEDCAT000000000000039","教师与教育","PROFESSION","适用职业",[94,104,105,106,107,108,109],"false","new","concept","user","new skill","workspace",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":116,"publishedAt":46},"01M6CVKVT6ZVYN2XPESS5EGDKE","sync-upstream-skills","Re-transplant owned skills from their mattpocock upstream after a git pull, applying upstream content changes while preserving opencode-native frontmatter. Also checks for duplicate skill names and dangling cross-skill references. Use when the user says \"sync skills\", \"refresh from upstream\", \"pull matt's skills\", \"sync upstream\", \"update the transplanted skills\", or wants to bring the owned transplanted skills up to date. Do not use to add or rename a transplanted skill — that stays manual.","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Fsync-upstream-skills\u002FSKILL.md",[],[117,118,119,19,120,30,121,45,122,123,124,125,126,38,26,127,128,129,107,130,35,131],"sync upstream","sync upstream skills","upstream","refresh","references","duplicate skill","duplicate","frontmatter","native","changes","re","for","bring","content","git",{"skillNo":133,"name":134,"nameZh":6,"description":135,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":136,"categories":137,"keywords":138,"publishedAt":46},"01M6CVKNEPEY1WA7E5FWQ85NJW","repograde","Grade student repositories with optional date filtering in single-repo or bulk mode. Use when the user says \"repograde\", \"grade the repos\", \"grade this repo since \u003Cdate>\", \"bewerte die Abgaben\", \"korrigiere die Repos\", or asks for a repo grading run over a class folder or a single student repository. Do not use for project grading with issues and PRs (use projectgrade) or for knowledge-check exams (use knowledge-assessment).","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Frepograde\u002FSKILL.md",[],[134,30,139,140,141,142,143,144,145,146,41,147],"with","repositories","single","student","mode","bulk","education","execution","filtering",{"skillNo":149,"name":150,"nameZh":6,"description":151,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":152,"categories":153,"keywords":154,"publishedAt":46},"01M6CVKF9MVFPHBH3W2D5ETEB6","projectgrade","Grade student project repositories based on Git commits, GitHub Issues, and Pull Requests. Use when the user says \"projectgrade\", \"grade the project\", \"bewerte das Projekt\", \"Projektarbeit benoten\", or asks for a holistic grading of a collaborative repository from inside it, based on commits, issues, reviews and PRs. Do not use for per-lesson assignments (use repograde) or for knowledge-check exams (use knowledge-assessment).","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Fprojectgrade\u002FSKILL.md",[],[150,155,156,140,157,142,158,145,131,159,30,160,146,161,162,45],"github issues","requests","git commits","project","github","pull requests","issues","commits",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":46},"01M6CVKA8J284DZTBE7QRN45SG","lehrplan","Richtet österreichische HTL-Unterrichts-Repositorien ein und pflegt sie — drei Kernaufgaben: das gesetzliche Lehrplan-Material eines Ausbildungszweigs (z. B. WII, WIT, KIF) aus dem RIS beschaffen inklusive jährlichem Novellen-Check, den Lehrplan eines konkreten Gegenstands aus dem Gesetz extrahieren (mit Erläuterung pro Lernziel: was der Inhalt tatsächlich IST, plus praktische Anwendungen), und Semesterpläne interaktiv entwerfen. Use when der Nutzer 'Lehrplan', 'Unterrichtsvorbereitung prüfen', 'Lehrplan auf Novellen prüfen', 'RIS sync', 'Gesetzesmaterial holen', 'Lehrplan extrahieren', 'Jahrespläne neu ziehen', 'Erläuterungen ergänzen', 'Semesterplan erstellen\u002Füberarbeiten' oder 'Einheiten erstellen' sagt, oder wenn in einem Repo gearbeitet wird, das der lehrplan\u002F-Konvention folgt. Do not use for Prepared Lessons (create-lesson), Aufgaben im Kohortenordner (homework) oder Wissensüberprüfungen (knowledge-exam).","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Flehrplan\u002FSKILL.md",[],[165,170,171,172,19,173,145,41,30,174],"material","pro","curriculum","check","drei",{"skillNo":176,"name":177,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":186,"publishedAt":46},"01M6CVK6QMR16MSMS2MRG097GV","knowledge-persistence","Persist session context into the structured docs\u002Fai knowledge files (OKF bundle; committed and pushed at the end). Use when the user says \"remember\", \"merk dir das\", \"save context\", \"persist knowledge\" or \"update knowledge file\", at a session checkpoint, or when an observation in this session contradicts docs\u002Fai\u002F or AGENTS.md. Do not use for reading the knowledge files at session start — that is plain bootstrapping.","https:\u002F\u002Fgithub.com\u002Fgeorgernstgraf\u002Fopencode-helpers\u002Fblob\u002Fmain\u002Fskills\u002Fknowledge-persistence\u002FSKILL.md",[181],{"categoryNo":182,"categoryName":183,"taxonomyType":184,"taxonomyTypeDesc":185},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[187,188,189,190,191,192,193,30,194,195,196,197,198],"knowledge persistence","knowledge","persistence","persist","structured","context","docs","documentation","workflow","files","session context","session"]