agents: add sync-git-branch-continuously skill (#4126)

Provide an automated background daemon and agent skill to continuously
synchronize Git branches with remote tracking repositories during agent
workflows.

Add the `sync-git-branch-continuously` skill and companion daemon
script. The daemon periodically performs fast-forward pulls and pushes
to configured remotes, while supporting asynchronous wakeup via
`SIGUSR1` signals for immediate on-demand synchronization cycles.
diff --git a/.agents/skills/sync-git-branch-continuously/SKILL.md b/.agents/skills/sync-git-branch-continuously/SKILL.md
new file mode 100644
index 0000000..06f6497
--- /dev/null
+++ b/.agents/skills/sync-git-branch-continuously/SKILL.md
@@ -0,0 +1,86 @@
+---
+name: sync-git-branch-continuously
+description: >-
+  Runs and manages an automated continuous synchronization loop (pull and push)
+  for Git branches. Use when asked to continuously sync, pull, or push a Git
+  branch in the background for Git repositories.
+---
+
+# Sync Git Branch Continuously
+
+Runs a continuous pull and push loop in the background for Git repositories to
+keep a local branch synchronized with remote branches. This helps ensure that
+new worktrees based off of main are more up to date.
+
+## Workflow Loop
+
+The sync loop periodically executes:
+
+1.  `git pull --ff-only <pull-from-remote> <pull-from-branch>`
+2.  `git push <push-to-remote> HEAD:<push-to-branch>`
+3.  Sleeps until timeout (e.g., 60s) or until awakened by an asynchronous
+    `SIGUSR1` signal.
+
+## Script Location
+
+The synchronization script is located at:
+`scripts/sync_git_branch_continuously.py` under this skill directory.
+
+## Running the Continuous Sync Script
+
+To start the continuous sync script as a background daemon process:
+
+```bash
+# Sync active branch with tracking remote every 60s (default):
+REPO_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
+BRANCH_NAME=$(git branch --show-current 2>/dev/null || echo "default")
+./.agents/skills/sync-git-branch-continuously/scripts/\
+sync_git_branch_continuously.py \
+  > "/tmp/sync_git_${REPO_NAME}_${BRANCH_NAME}.log" 2>&1 &
+```
+
+### Script Options
+
+*   `--pull-from-branch <name>`: Remote branch to pull from (defaults to active
+    branch).
+*   `--push-to-branch <name>`: Remote branch to push to (defaults to active
+    branch).
+*   `--pull-from-remote <name>`: Remote repository to pull from (defaults to
+    tracking remote or `origin`).
+*   `--push-to-remote <name>`: Remote repository to push to (defaults to
+    tracking remote or `origin`).
+*   `--interval <seconds>`: Sleep interval between sync iterations (default:
+    `60` seconds).
+
+Example with custom source and target destinations:
+
+```bash
+./.agents/skills/sync-git-branch-continuously/scripts/\
+sync_git_branch_continuously.py \
+  --pull-from-remote upstream --pull-from-branch main \
+  --push-to-remote origin --push-to-branch feature \
+  --interval 30 \
+  > "/tmp/sync_git_main.log" 2>&1 &
+```
+
+## Asynchronous Sync Triggers (Signals)
+
+Other processes or agent conversations can asynchronously wake up the daemon
+and trigger an immediate pull/push cycle without waiting for the interval to
+expire:
+
+```bash
+# By PID:
+kill -USR1 <PID>
+
+# Or by matching process name:
+pkill -USR1 -f "sync_git_branch_continuously.py"
+```
+
+## Monitoring & Logs
+
+To check the status or inspect the output of the active background sync loop:
+
+```bash
+tail -n 20 "/tmp/sync_git_${REPO_NAME}_${BRANCH_NAME}.log"
+```
diff --git a/.agents/skills/sync-git-branch-continuously/scripts/sync_git_branch_continuously.py b/.agents/skills/sync-git-branch-continuously/scripts/sync_git_branch_continuously.py
new file mode 100755
index 0000000..47d62b0
--- /dev/null
+++ b/.agents/skills/sync-git-branch-continuously/scripts/sync_git_branch_continuously.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+"""Continuously pulls and pushes a Git branch with fast-forward updates.
+
+Usage:
+  sync_git_branch_continuously.py [OPTIONS]
+
+Example:
+  sync_git_branch_continuously.py --pull-from-remote upstream --pull-from-branch main \
+      --push-to-remote origin --push-to-branch feature --interval 30
+
+Triggering asynchronous sync:
+  kill -USR1 <PID>
+"""
+
+import argparse
+import os
+import shlex
+import signal
+import subprocess
+import sys
+import threading
+from datetime import datetime
+
+_SYNC_TRIGGER = threading.Event()
+
+
+def handle_sync_signal(signum: int, unused_frame: object) -> None:
+    """Signal handler that triggers an immediate synchronization cycle."""
+    sig_name = signal.Signals(signum).name
+    print(
+        f"\n[Signal] Received {sig_name} ({signum}), triggering immediate sync...",
+        flush=True,
+    )
+    _SYNC_TRIGGER.set()
+
+
+def register_signal_handlers() -> None:
+    """Registers signal handlers for asynchronous wakeups."""
+    signal.signal(signal.SIGUSR1, handle_sync_signal)
+
+
+def run_command(
+    cmd: list[str],
+    capture_output: bool = False,
+    print_cmd: bool = True,
+) -> subprocess.CompletedProcess[str]:
+    """Runs a subprocess command, optionally capturing output and printing command details."""
+    if print_cmd:
+        print(f"+ {shlex.join(cmd)}", flush=True)
+    return subprocess.run(
+        cmd,
+        capture_output=capture_output,
+        text=True,
+        check=False,
+    )
+
+
+def get_current_branch() -> str:
+    """Returns the name of the currently checked out Git branch."""
+    result = run_command(
+        ["git", "branch", "--show-current"],
+        capture_output=True,
+        print_cmd=False,
+    )
+    branch = result.stdout.strip()
+    if branch:
+        return branch
+
+    result = run_command(
+        ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+        capture_output=True,
+        print_cmd=False,
+    )
+    return result.stdout.strip()
+
+
+def get_tracking_remote(branch: str) -> str:
+    """Returns the configured remote for a branch, defaulting to 'origin'."""
+    result = run_command(
+        ["git", "config", f"branch.{branch}.remote"],
+        capture_output=True,
+        print_cmd=False,
+    )
+    remote = result.stdout.strip()
+    return remote if remote else "origin"
+
+
+def sync_iteration(
+    pull_remote: str,
+    pull_branch: str,
+    push_remote: str,
+    push_branch: str,
+) -> bool:
+    """Performs one pull and push synchronization cycle."""
+    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+    print(
+        f"[{timestamp}] Pulling with --ff-only ({pull_remote}/{pull_branch})...",
+        flush=True,
+    )
+    pull_cmd = ["git", "pull", "--ff-only", pull_remote, pull_branch]
+    if run_command(pull_cmd).returncode != 0:
+        print(
+            f"[{timestamp}] Warning: git pull --ff-only failed. Will retry next"
+            " interval.",
+            file=sys.stderr,
+            flush=True,
+        )
+        return False
+
+    print(f"[{timestamp}] Pushing ({push_remote} HEAD:{push_branch})...", flush=True)
+    push_cmd = ["git", "push", push_remote, f"HEAD:{push_branch}"]
+    if run_command(push_cmd).returncode != 0:
+        print(
+            f"[{timestamp}] Warning: git push failed. Will retry next interval.",
+            file=sys.stderr,
+            flush=True,
+        )
+        return False
+
+    print(f"[{timestamp}] Sync successful.", flush=True)
+    return True
+
+
+def parse_args() -> argparse.Namespace:
+    """Parses command-line arguments."""
+    parser = argparse.ArgumentParser(
+        description="Continuously pull and push a Git branch with fast-forward updates."
+    )
+    parser.add_argument(
+        "--pull-from-branch",
+        type=str,
+        default=None,
+        help="Remote branch to pull from (default: active branch)",
+    )
+    parser.add_argument(
+        "--push-to-branch",
+        type=str,
+        default=None,
+        help="Remote branch to push to (default: active branch)",
+    )
+    parser.add_argument(
+        "--pull-from-remote",
+        type=str,
+        default=None,
+        help="Remote repository to pull from (default: tracking remote or origin)",
+    )
+    parser.add_argument(
+        "--push-to-remote",
+        type=str,
+        default=None,
+        help="Remote repository to push to (default: tracking remote or origin)",
+    )
+    parser.add_argument(
+        "--interval",
+        type=int,
+        default=60,
+        help="Sleep interval in seconds between syncs (default: 60)",
+    )
+    return parser.parse_args()
+
+
+def main() -> None:
+    """Main entrypoint for the continuous sync loop."""
+    args = parse_args()
+
+    # Verify inside git repo
+    check_repo = run_command(
+        ["git", "rev-parse", "--is-inside-work-tree"],
+        capture_output=True,
+        print_cmd=False,
+    )
+    if check_repo.returncode != 0:
+        print("Error: Not inside a Git working tree.", file=sys.stderr, flush=True)
+        sys.exit(1)
+
+    active_branch = get_current_branch()
+    pull_branch = args.pull_from_branch or active_branch
+    push_branch = args.push_to_branch or active_branch
+
+    if not pull_branch or pull_branch == "HEAD":
+        print(
+            "Error: Could not determine pull-from-branch. Please specify"
+            " --pull-from-branch.",
+            file=sys.stderr,
+            flush=True,
+        )
+        sys.exit(1)
+
+    if not push_branch or push_branch == "HEAD":
+        print(
+            "Error: Could not determine push-to-branch. Please specify"
+            " --push-to-branch.",
+            file=sys.stderr,
+            flush=True,
+        )
+        sys.exit(1)
+
+    pull_remote = args.pull_from_remote or get_tracking_remote(pull_branch)
+    push_remote = args.push_to_remote or get_tracking_remote(push_branch)
+
+    register_signal_handlers()
+
+    pid = os.getpid()
+    print(
+        f"Starting continuous Git sync (PID: {pid}): pull from"
+        f" '{pull_remote}/{pull_branch}', push to"
+        f" '{push_remote}/{push_branch}' every {args.interval}s...",
+        flush=True,
+    )
+    print(
+        f"To trigger immediate sync: kill -USR1 {pid}",
+        flush=True,
+    )
+
+    while True:
+        _SYNC_TRIGGER.clear()
+        sync_iteration(pull_remote, pull_branch, push_remote, push_branch)
+        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        print(
+            f"[{timestamp}] Sleeping for {args.interval}s (waiting for timeout or"
+            f" kill -USR1 {pid})...",
+            flush=True,
+        )
+        _SYNC_TRIGGER.wait(timeout=args.interval)
+
+
+if __name__ == "__main__":
+    main()