Synthesis Repo Guard
The Problem
AI coding assistants and project-management tooling create and modify files continuously. A stopped task needs lightweight same-machine recovery immediately, while another computer needs a deliberate publication boundary. Treating both cases as an automatic commit creates noisy history and network latency; treating neither creates invisible local-only state. Tools that write files outside agent sessions (a project console writing status markers, manual edits) need the same attribution contract.
v1 of this skill detected stranded state and alerted with a count ("N repositories have unsynced changes"). Two failures emerged in practice:
- The alert was unactionable — and leaky if made actionable. A count says nothing useful; speaking repo names would fix that, but repo/workspace names are often client names, and audio reaches whoever is nearby or on an unmuted call. Notification banners leak the same way during screen-shares.
- The alert fired on machine-fixable states. Most unsynced state is exactly what automation should heal at the next sensible checkpoint. Alerting humans about machine-fixable problems trains them to ignore alerts.
The Architecture — three layers
| Layer |
Component |
Job |
| Detector |
repo_sync_check.py (scan) |
Find dirty / ahead / behind / detached repos under a workspace root |
| Messenger |
repo_sync_check.py (output) |
Generic audio/banner ping + detailed report files + console tile data |
| Checkpointer |
checkpoint_sync.py |
Record local handoffs; batch exact context paths at explicit remote-sync events |
End state: same-computer client switching is filesystem-local and fast.
Cross-computer publication is batched and explicit. The synthesis-console shows
ambient status, and alerts remain rare and actionable.
Confidentiality rule for alert surfaces (ABSOLUTE)
Audio (say, alert sounds) and macOS notification banners never carry repo names, workspace names, or client names — only counts and a pointer ("details are in your synthesis console"). This holds at all times, not only while screen-sharing: presence detection is unreliable, and one leak outweighs the convenience. Identifying detail belongs exclusively in pull channels the user deliberately opens:
~/.synthesis/repo-guard/last-report.txt / last-report.json / history.jsonl — written on every scan
~/.synthesis/repo-guard/checkpoint-state.json — written on every checkpoint run
- the synthesis-console sync tile / page, which renders both
Mute toggle: all audible output (speech AND alert sounds) is suppressed while ~/.synthesis/quiet-audio exists. synthesis-console exposes this as a header button; touch/rm the file works too. Muting loses nothing — reports and tile stay current.
Detection vs. commit — scoping rules
repo_sync_check.py detects and never modifies — correct scope: every repo in the workspace.
There are two separate readiness transitions:
- Local handoff: PostToolUse records structured edits by one client session; paired shell snapshots add net-new formatter, generator, and bulk-rewrite output without claiming unchanged pre-existing dirty paths. Stop writes an atomic receipt with branch, HEAD, file state, and content hashes. It performs no Git commit and no network call. If the client is interrupted before Stop, the pending manifest makes the work LOCAL_RECOVERABLE on the same filesystem.
- Remote handoff: the flush-pending command batches only private project-context paths into exact-path commits. Source paths remain owned by their repository workflow and must already be clean and equal to their upstream before manifests retire.
The checkpointer: local by default, remote by explicit event
checkpoint_sync.py runs at workflow events:
- AI-tool Stop: writes a local receipt when that client session has
attributed repository changes. A Stop with no attributed repository changes
is a cheap no-op because there is no new file state to preserve.
- After a console cockpit write:
--repo <written-file> --now records
a local producer manifest and receipt.
- Day-end / mac-sync:
--flush-pending publishes exact private-context
paths after the owning workflows publish any source paths.
Deliberately not a launchd or cron job. Wall-clock mutation can race
repositories across machines. Local receipts follow edit events; remote
mutation occurs only when the user invokes cross-machine sync or as part of
day-end. Read-only console polling remains safe.
The auto-sync class + runtime guard
Config ~/.synthesis/checkpoint-sync.yaml (copy checkpoint-sync.example.yaml) lists the class by explicit path and glob. Membership criteria: private knowledge/context repos (personal ai-knowledge repos, *-<person>-private workspace repos, daily plans). A configured checkout's isolated git worktrees inherit membership through their shared git-common-dir identity. Never source-code repos, never shared/public repos.
The runtime remote guard is independent of config: a repo is touched only if EVERY push remote starts with an allowed prefix (your private GitHub namespace). A glob that accidentally matches a repo with a client/org remote is excluded at run time, every time — config declares intent; the guard verifies reality. Empty allowed_remote_prefixes fails closed.
Safety properties
- Stop never commits, pushes, fetches, stages, or changes branches.
- Shell attribution compares pre/post Git state and fails closed when its
pre-tool snapshot is absent, unsafe, or belongs to another session.
- Remote publication orders exact-path context commit, fetch, then
fast-forward push. Existing staged or dirty files outside the manifest
remain untouched.
- Manifest writers, Stop receipts, remote flushes, and worktree retirement use
one lifecycle lock. Retirement pins a freshly fetched remote-tracking
commit, fsyncs a resumable intent before removal, invalidates old receipts,
and completes idempotently after interruption. A missing worktree without
this proof is reported as a
stranded entry that blocks readiness (see
below) rather than silently attributed elsewhere. Deleted files and child
directories within a verified live repository are recorded as missing,
without restoring them or discarding their pending attribution. Resolution
refuses symlink ancestry, unavailable worktree inventory and a missing
registered nested worktree rather than borrowing the enclosing repository.
- A distinct commit author identifies batched remote-context commits.
- Divergence leaves the exact commit and manifest local and reports the
block. Never rebase or force-push.
- Pre-commit hooks run normally. Never bypass them.
- Source paths and remotely publishable context paths are distinct fields
in each client-session manifest.
- A first commit on a feature branch publishes that exact branch with an
upstream.
- Active and stale Git index locks are reported and never deleted.
- A successful edit leaves a manifest even if Stop never runs. Remote
publication retains manifests until source and context paths are verified
upstream-current.
Stranded entries and per-repository retirement
A manifest entry is stranded when its path is missing, its nearest
existing ancestor sits outside every git working tree, and so the first
missing component beneath that ancestor is a worktree root that no longer
exists — a worktree removed before any retirement intent or Stop receipt could
name it. Stop and flush report it as stranded, name the missing worktree
root, and name the one accepted remedy. Other repositories in the same
manifest are still evaluated. A deleted file whose repository still resolves
is not stranded; it stays deleted-or-missing. Classification needs git's
answer: when git rev-parse cannot run for the nearest existing ancestor (a
timeout, no binary) the entry is failed as stranded classification unavailable, never stranded, and nothing is drop-eligible; when git refuses
a live repository (a safe.directory refusal, a damaged gitdir) the .git
entry visible in the ancestor chain keeps the existing handling. A transient
git failure never discards a live repository's pending attribution.
- Evidence outranks assertion. When a retirement intent names the worktree,
the remedy is
--complete-worktree-retirement INTENT (prepared) or the
intent's own verified head through --reconcile-retired-worktree
(completed). When this session's retained receipt names it with a
local-ready head, the remedy is --reconcile-retired-worktree WORKTREE --retirement-session ID while that receipt is LOCAL_READY and still binds
the current manifest digest; a receipt the manifest outgrew (edits accreted
after Stop wrote it) names the receipt's own head through
--retirement-head instead, which retires only what that head proves.
Stop leaves that receipt unchanged so the evidence survives.
- With neither, the entry is drop-eligible:
--flush-session ID --drop-stranded --assert "<why the work is known published>".
The drop recomputes the stranded set at run time, refuses if the worktree
root exists again, writes an append-only record to
~/.synthesis/repo-guard/retired-pending/<manifest>-stranded-<UTC>.json
(session, dropped paths, nearest existing ancestor, missing worktree root,
whether an intent or receipt named it, any repository whose HEAD tracks the
same relative path with its blob oid, the assertion, the acting identity,
the timestamp), then rewrites the manifest without those entries and
continues the normal flush. A blank --assert (whitespace or invisible
format characters only) is refused. --dry-run
reports the drop and writes nothing. Existing ledger records are never
replaced.
- Every flush that is not a dry run retires the entries of each repository
whose result is
clean, committed-pushed, pushed-stranded
(earlier commits pushed now) or source-remote-ready, keeps the blocked
repositories' entries, and deletes the manifest only once it is empty. The
retired-repositories result names what was retired. A long-lived session
therefore stops accreting already-published work onto a manifest one
blocked repository keeps alive.
Quick Start
# Scan ~/workspaces, write reports, print text summary
./repo_sync_check.py
# Machine-readable scan (console tile source)
./repo_sync_check.py --json --quiet
# Generic attention ping if dirty (mute-aware)
./repo_sync_check.py --speak --notify --dirty-only
# Preview pending remote publication
./checkpoint_sync.py --dry-run
# Record a same-machine Stop receipt
./checkpoint_sync.py --hook --quiet --notify
# Record a just-written producer file locally
./checkpoint_sync.py --repo ~/workspaces/example/daily-plans/today.md --now
# Publish pending project context after source repos are upstream-current
./checkpoint_sync.py --flush-pending
# Publish and retire one exact session without inspecting unrelated sessions
./checkpoint_sync.py --flush-session <session-id>
# Drop stranded entries (removed worktree, no retained evidence) under a recorded assertion
./checkpoint_sync.py --flush-session <session-id> --drop-stranded --assert "merged to main on 2026-08-31"
Exit codes (both scripts)
| Code |
repo_sync_check.py |
checkpoint_sync.py |
| 0 |
all clean & synced |
requested readiness reached |
| 1 |
repos need attention |
alerts raised (detail in state file) |
| 2 |
error |
error |
What the detector reports
| Condition |
Marker |
| Uncommitted changes (modified/staged/untracked) |
[dirty] + file list + fix hint |
| Unpushed commits |
[ahead] + count |
| Unpulled commits |
[behind] + count |
| Detached HEAD |
[detached] |
| Git errors |
[error] |
AI Tool Integration
Claude Code (~/.claude/settings.json)
Turn-end remediation of the current session's attributed context paths plus
optional session-end verification:
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command",
"command": "python3 <synthesis-repo-guard-root>/checkpoint_sync.py --hook --quiet --notify",
"timeout": 120 } ] }
],
"SessionEnd": [
{ "hooks": [ { "type": "command",
"command": "python3 <synthesis-repo-guard-root>/repo_sync_check.py --dirty-only --speak --notify",
"timeout": 60 } ] }
]
}
}
OpenAI Codex (~/.codex/hooks.json, with features.hooks = true)
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command",
"command": "python3 <synthesis-repo-guard-root>/checkpoint_sync.py --hook --quiet --notify",
"timeout": 120 } ] } ] } }
Each client session has its own hashed pending manifest under
~/.synthesis/repo-guard/pending/. Multiple agents can therefore coexist
without one hook committing, publishing, or overwriting another session's files.
For projects that adopt CURRENT_STATE.json, the plugin's provider-neutral
Stop gate composes this file-level evidence with synthesis project management's
semantic checkpoint. A clean project handoff receipt is issued only when the
current coordination seat, project id, Git identity, durable-file hashes, and
recorded source heads all match. An interrupted task still relies on the
pending manifest and reports LOCAL_RECOVERABLE; it is never mislabeled clean.
Cursor (.cursor/settings.json)
{ "task.onEnd": "python3 /path/to/checkpoint_sync.py --hook --quiet --notify" }
synthesis-console (command center)
- Always-on sync tile: polls
repo_sync_check.py --json --quiet (read-only; lid-safe) and renders checkpoint-state.json outcomes.
- Quiet-audio toggle button: creates/removes
~/.synthesis/quiet-audio.
- "Sync now" button:
checkpoint_sync.py --no-throttle, an explicit remote-context handoff alias.
- Producer receipts: after writing a plan marker, the console records local state with
--repo <file> --now.
Scheduled execution — read-only only
If a tool supports no hooks at all, a scheduled detector run (repo_sync_check.py --quiet, reports only, no audio flags) is acceptable — it's read-only and interruption-safe. Do not schedule checkpoint_sync.py: mutation stays event-driven (see design rationale above). The console tile's polling normally makes scheduled detection unnecessary.
Relationship to Other Skills
- synthesis-mac-sync — the full multi-machine sync operation (config files, credentials, all repos, with user approval). Repo-guard keeps same-machine work recoverable; mac-sync owns the explicit cross-machine publication transition.
- synthesis-context-lifecycle / synthesis-daily-rituals — those skills keep local project state current during work and publish it through remote handoff or day-end.
repo_sync_check.py is the final day-end verification gate.
Command Reference
repo_sync_check.py [--workspace W] [--max-depth N] [--quiet] [--json]
[--dirty-only] [--alert] [--speak] [--notify]
[--report-dir D] [--no-report]
checkpoint_sync.py [--config C] [--repo PATH] [--hook] [--now]
[--flush-pending | --flush-session SESSION_ID
[--drop-stranded --assert TEXT]]
[--no-throttle] [--dry-run]
[--prepare-worktree-retirement PATH
--retirement-repository REPO --retirement-head SHA
--retirement-remote REMOTE --retirement-base REMOTE_REF]
[--complete-worktree-retirement INTENT]
[--reconcile-retired-worktree PATH
--retirement-repository REPO
(--retirement-head SHA | --retirement-session SESSION_ID)
--retirement-remote REMOTE --retirement-base REMOTE_REF]
[--quiet] [--json]
[--speak] [--notify]
--speak/--notify/--alert are generic + mute-aware on both scripts.
checkpoint_sync --repo records one configured producer path locally; it does not commit or use the network.
--hook consumes the calling session's JSON hook payload and never falls
back to a workspace-wide mutation. --flush-pending is the explicit
remote-context transition; --no-throttle is its console compatibility alias.
--flush-session applies the same remote-readiness gates to one manifest
selected by its exact session id. It does not read, validate, publish, or
delete any other session manifest. Use it when unrelated pending work must
remain recoverable while one fully published session transitions to
REMOTE_READY.
--drop-stranded --assert TEXT is valid only with --flush-session. It
retires drop-eligible stranded entries after recording the operator's
assertion in the retired-pending ledger; see "Stranded entries and
per-repository retirement" above.
--retirement-session derives a removed worktree's historical head from
this native session's retained local receipt. It verifies the manifest
digest, attributed bytes/deletions and file modes, and remote ancestry;
it never substitutes current file existence for preservation evidence.
Only that session's manifest is changed. The retirement transaction retains
exact post-images for crash recovery and preserves evidence for remaining
worktrees. Missing older evidence is an explicit recovery gap. See
checkpoint closure recovery.
Design Principles
- Zero AI and zero external dependencies — Python stdlib + git CLI (PyYAML used if present, minimal built-in parser otherwise)
- LLM-agnostic — same scripts for Claude Code, Codex, Cursor, console, or manual use
- Detector never modifies; Stop is local-only; remote publication modifies only exact guarded context paths
- Identifying names (repo, workspace, client) never on audio/banner surfaces — counts and pointers only
- Remote mutation is explicit or day-end; only reads may poll
- Fail closed, never force — empty guard config disables; divergence/hook-failures stop and alert
- Composable — exit codes, JSON output, shared state files
Changelog
- 2.5.0 (2026-09-11): classifies manifest entries beneath a removed,
unrecorded worktree as
stranded instead of aborting the whole manifest;
adds --flush-session ID --drop-stranded --assert TEXT with an append-only
retired-pending ledger; retires published repositories' entries per flush
so blocked repositories no longer keep already-published work pending.
- 2.3.0 (2026-08-23): adds a fail-closed exact-session remote handoff that
retires one verified manifest without coupling it to unrelated pending
sessions; preserves global flush behavior and fsyncs successful manifest
retirement.
- 2.2.0 (2026-08-14): makes retirement a lifecycle-locked, remote-pinned,
fsynced, resumable transaction across manifests and receipts; resumes with
the intent's exact content-addressed reconciler and compare-binds optional
remote branch deletion to the verified head; keeps unexplained missing and
unpublished paths fail-closed.
- 2.1.1 (2026-08-14): clarifies that a clean no-edit task needs no empty receipt and that remote readiness compares complete branch heads rather than project-path history.
- 2.1.0 (2026-08-14): separates local and remote readiness. Stop records atomic client-session receipts without Git or network mutation; interruption leaves a recoverable manifest; explicit remote handoff batches exact private-context paths only after source paths are upstream-current. Adds worktree identity, first-branch publication, generic commit messages, staged-index isolation, and integration fixtures.
- 2.0.0 (2026-07-08): three-layer redesign. Generic-only audio/banner (confidentiality rule),
~/.synthesis/quiet-audio mute flag, report files + history, remediation hints, new checkpoint_sync.py (event-driven auto-commit/push: runtime remote guard, quiescence, shared throttle, ff-only push, distinct author, stale-lock detection), synthesis-console integration contract, scheduled-mutation explicitly disallowed. Origin: 2026-07-08 design review (lesson: alert-channel confidentiality + event-driven checkpoints).
- 1.1.0: detector + count-only audio alerts.
1---2name: synthesis-repo-guard3description: Workspace git-sync guard: detects unsynced repos, records session-attributed local handoff receipts, and batches private project-context commits for explicit remote handoff or day-end. Reports through confidentiality-safe channels.4license: Apache-2.05---67# Synthesis Repo Guard89## The Problem1011AI coding assistants and project-management tooling create and modify files continuously. A stopped task needs lightweight same-machine recovery immediately, while another computer needs a deliberate publication boundary. Treating both cases as an automatic commit creates noisy history and network latency; treating neither creates invisible local-only state. Tools that write files *outside* agent sessions (a project console writing status markers, manual edits) need the same attribution contract.1213v1 of this skill detected stranded state and alerted with a count ("N repositories have unsynced changes"). Two failures emerged in practice:14151. **The alert was unactionable — and leaky if made actionable.** A count says nothing useful; speaking repo names would fix that, but repo/workspace names are often client names, and audio reaches whoever is nearby or on an unmuted call. Notification banners leak the same way during screen-shares.162. **The alert fired on machine-fixable states.** Most unsynced state is exactly what automation should heal at the next sensible checkpoint. Alerting humans about machine-fixable problems trains them to ignore alerts.1718## The Architecture — three layers1920| Layer | Component | Job |21|-------|-----------|-----|22| Detector | `repo_sync_check.py` (scan) | Find dirty / ahead / behind / detached repos under a workspace root |23| Messenger | `repo_sync_check.py` (output) | Generic audio/banner ping + detailed report files + console tile data |24| Checkpointer | `checkpoint_sync.py` | Record local handoffs; batch exact context paths at explicit remote-sync events |2526End state: same-computer client switching is filesystem-local and fast.27Cross-computer publication is batched and explicit. The synthesis-console shows28ambient status, and alerts remain rare and actionable.2930## Confidentiality rule for alert surfaces (ABSOLUTE)3132**Audio (`say`, alert sounds) and macOS notification banners never carry repo names, workspace names, or client names — only counts and a pointer ("details are in your synthesis console").** This holds at all times, not only while screen-sharing: presence detection is unreliable, and one leak outweighs the convenience. Identifying detail belongs exclusively in pull channels the user deliberately opens:3334- `~/.synthesis/repo-guard/last-report.txt` / `last-report.json` / `history.jsonl` — written on every scan35- `~/.synthesis/repo-guard/checkpoint-state.json` — written on every checkpoint run36- the synthesis-console sync tile / page, which renders both3738**Mute toggle:** all audible output (speech AND alert sounds) is suppressed while `~/.synthesis/quiet-audio` exists. synthesis-console exposes this as a header button; `touch`/`rm` the file works too. Muting loses nothing — reports and tile stay current.3940## Detection vs. commit — scoping rules4142`repo_sync_check.py` **detects and never modifies** — correct scope: every repo in the workspace.4344There are two separate readiness transitions:45461. **Local handoff:** PostToolUse records structured edits by one client session; paired shell snapshots add net-new formatter, generator, and bulk-rewrite output without claiming unchanged pre-existing dirty paths. Stop writes an atomic receipt with branch, HEAD, file state, and content hashes. It performs no Git commit and no network call. If the client is interrupted before Stop, the pending manifest makes the work LOCAL_RECOVERABLE on the same filesystem.472. **Remote handoff:** the flush-pending command batches only private project-context paths into exact-path commits. Source paths remain owned by their repository workflow and must already be clean and equal to their upstream before manifests retire.4849## The checkpointer: local by default, remote by explicit event5051`checkpoint_sync.py` runs at workflow events:5253- **AI-tool Stop:** writes a local receipt when that client session has54 attributed repository changes. A Stop with no attributed repository changes55 is a cheap no-op because there is no new file state to preserve.56- **After a console cockpit write:** `--repo <written-file> --now` records57 a local producer manifest and receipt.58- **Day-end / mac-sync:** `--flush-pending` publishes exact private-context59 paths after the owning workflows publish any source paths.6061**Deliberately not a launchd or cron job.** Wall-clock mutation can race62repositories across machines. Local receipts follow edit events; remote63mutation occurs only when the user invokes cross-machine sync or as part of64day-end. Read-only console polling remains safe.6566### The auto-sync class + runtime guard6768Config `~/.synthesis/checkpoint-sync.yaml` (copy `checkpoint-sync.example.yaml`) lists the class by explicit path and glob. Membership criteria: private knowledge/context repos (personal ai-knowledge repos, `*-<person>-private` workspace repos, daily plans). A configured checkout's isolated git worktrees inherit membership through their shared git-common-dir identity. Never source-code repos, never shared/public repos.6970**The runtime remote guard is independent of config:** a repo is touched only if EVERY push remote starts with an allowed prefix (your private GitHub namespace). A glob that accidentally matches a repo with a client/org remote is excluded at run time, every time — config declares intent; the guard verifies reality. Empty `allowed_remote_prefixes` fails closed.7172### Safety properties7374- Stop never commits, pushes, fetches, stages, or changes branches.75- Shell attribution compares pre/post Git state and fails closed when its76 pre-tool snapshot is absent, unsafe, or belongs to another session.77- Remote publication orders exact-path context commit, fetch, then78 fast-forward push. Existing staged or dirty files outside the manifest79 remain untouched.80- Manifest writers, Stop receipts, remote flushes, and worktree retirement use81 one lifecycle lock. Retirement pins a freshly fetched remote-tracking82 commit, fsyncs a resumable intent before removal, invalidates old receipts,83 and completes idempotently after interruption. A missing worktree without84 this proof is reported as a `stranded` entry that blocks readiness (see85 below) rather than silently attributed elsewhere. Deleted files and child86 directories within a verified live repository are recorded as missing,87 without restoring them or discarding their pending attribution. Resolution88 refuses symlink ancestry, unavailable worktree inventory and a missing89 registered nested worktree rather than borrowing the enclosing repository.90- A distinct commit author identifies batched remote-context commits.91- Divergence leaves the exact commit and manifest local and reports the92 block. Never rebase or force-push.93- Pre-commit hooks run normally. Never bypass them.94- Source paths and remotely publishable context paths are distinct fields95 in each client-session manifest.96- A first commit on a feature branch publishes that exact branch with an97 upstream.98- Active and stale Git index locks are reported and never deleted.99- A successful edit leaves a manifest even if Stop never runs. Remote100 publication retains manifests until source and context paths are verified101 upstream-current.102103### Stranded entries and per-repository retirement104105A manifest entry is **stranded** when its path is missing, its nearest106existing ancestor sits outside every git working tree, and so the first107missing component beneath that ancestor is a worktree root that no longer108exists — a worktree removed before any retirement intent or Stop receipt could109name it. Stop and flush report it as `stranded`, name the missing worktree110root, and name the one accepted remedy. Other repositories in the same111manifest are still evaluated. A deleted file whose repository still resolves112is not stranded; it stays `deleted-or-missing`. Classification needs git's113answer: when `git rev-parse` cannot run for the nearest existing ancestor (a114timeout, no binary) the entry is `failed` as `stranded classification115unavailable`, never `stranded`, and nothing is drop-eligible; when git refuses116a live repository (a safe.directory refusal, a damaged gitdir) the `.git`117entry visible in the ancestor chain keeps the existing handling. A transient118git failure never discards a live repository's pending attribution.119120- Evidence outranks assertion. When a retirement intent names the worktree,121 the remedy is `--complete-worktree-retirement INTENT` (prepared) or the122 intent's own verified head through `--reconcile-retired-worktree`123 (completed). When this session's retained receipt names it with a124 local-ready head, the remedy is `--reconcile-retired-worktree WORKTREE125 --retirement-session ID` while that receipt is LOCAL_READY and still binds126 the current manifest digest; a receipt the manifest outgrew (edits accreted127 after Stop wrote it) names the receipt's own head through128 `--retirement-head` instead, which retires only what that head proves.129 Stop leaves that receipt unchanged so the evidence survives.130- With neither, the entry is drop-eligible:131 `--flush-session ID --drop-stranded --assert "<why the work is known published>"`.132 The drop recomputes the stranded set at run time, refuses if the worktree133 root exists again, writes an append-only record to134 `~/.synthesis/repo-guard/retired-pending/<manifest>-stranded-<UTC>.json`135 (session, dropped paths, nearest existing ancestor, missing worktree root,136 whether an intent or receipt named it, any repository whose HEAD tracks the137 same relative path with its blob oid, the assertion, the acting identity,138 the timestamp), then rewrites the manifest without those entries and139 continues the normal flush. A blank `--assert` (whitespace or invisible140 format characters only) is refused. `--dry-run`141 reports the drop and writes nothing. Existing ledger records are never142 replaced.143- Every flush that is not a dry run retires the entries of each repository144 whose result is `clean`, `committed-pushed`, `pushed-stranded`145 (earlier commits pushed now) or `source-remote-ready`, keeps the blocked146 repositories' entries, and deletes the manifest only once it is empty. The147 `retired-repositories` result names what was retired. A long-lived session148 therefore stops accreting already-published work onto a manifest one149 blocked repository keeps alive.150151---152153## Quick Start154155```bash156# Scan ~/workspaces, write reports, print text summary157./repo_sync_check.py158159# Machine-readable scan (console tile source)160./repo_sync_check.py --json --quiet161162# Generic attention ping if dirty (mute-aware)163./repo_sync_check.py --speak --notify --dirty-only164165# Preview pending remote publication166./checkpoint_sync.py --dry-run167168# Record a same-machine Stop receipt169./checkpoint_sync.py --hook --quiet --notify170171# Record a just-written producer file locally172./checkpoint_sync.py --repo ~/workspaces/example/daily-plans/today.md --now173174# Publish pending project context after source repos are upstream-current175./checkpoint_sync.py --flush-pending176177# Publish and retire one exact session without inspecting unrelated sessions178./checkpoint_sync.py --flush-session <session-id>179180# Drop stranded entries (removed worktree, no retained evidence) under a recorded assertion181./checkpoint_sync.py --flush-session <session-id> --drop-stranded --assert "merged to main on 2026-08-31"182```183184### Exit codes (both scripts)185186| Code | repo_sync_check.py | checkpoint_sync.py |187|------|--------------------|--------------------|188| 0 | all clean & synced | requested readiness reached |189| 1 | repos need attention | alerts raised (detail in state file) |190| 2 | error | error |191192### What the detector reports193194| Condition | Marker |195|-----------|--------|196| Uncommitted changes (modified/staged/untracked) | `[dirty]` + file list + fix hint |197| Unpushed commits | `[ahead]` + count |198| Unpulled commits | `[behind]` + count |199| Detached HEAD | `[detached]` |200| Git errors | `[error]` |201202---203204## AI Tool Integration205206### Claude Code (`~/.claude/settings.json`)207208Turn-end remediation of the current session's attributed context paths plus209optional session-end verification:210211```json212{213 "hooks": {214 "Stop": [215 { "hooks": [ { "type": "command",216 "command": "python3 <synthesis-repo-guard-root>/checkpoint_sync.py --hook --quiet --notify",217 "timeout": 120 } ] }218 ],219 "SessionEnd": [220 { "hooks": [ { "type": "command",221 "command": "python3 <synthesis-repo-guard-root>/repo_sync_check.py --dirty-only --speak --notify",222 "timeout": 60 } ] }223 ]224 }225}226```227228### OpenAI Codex (`~/.codex/hooks.json`, with `features.hooks = true`)229230```json231{ "hooks": { "Stop": [ { "hooks": [ { "type": "command",232 "command": "python3 <synthesis-repo-guard-root>/checkpoint_sync.py --hook --quiet --notify",233 "timeout": 120 } ] } ] } }234```235236Each client session has its own hashed pending manifest under237`~/.synthesis/repo-guard/pending/`. Multiple agents can therefore coexist238without one hook committing, publishing, or overwriting another session's files.239240For projects that adopt `CURRENT_STATE.json`, the plugin's provider-neutral241Stop gate composes this file-level evidence with synthesis project management's242semantic checkpoint. A clean project handoff receipt is issued only when the243current coordination seat, project id, Git identity, durable-file hashes, and244recorded source heads all match. An interrupted task still relies on the245pending manifest and reports `LOCAL_RECOVERABLE`; it is never mislabeled clean.246247### Cursor (`.cursor/settings.json`)248249```json250{ "task.onEnd": "python3 /path/to/checkpoint_sync.py --hook --quiet --notify" }251```252253### synthesis-console (command center)254255- **Always-on sync tile:** polls `repo_sync_check.py --json --quiet` (read-only; lid-safe) and renders `checkpoint-state.json` outcomes.256- **Quiet-audio toggle button:** creates/removes `~/.synthesis/quiet-audio`.257- **"Sync now" button:** `checkpoint_sync.py --no-throttle`, an explicit remote-context handoff alias.258- **Producer receipts:** after writing a plan marker, the console records local state with `--repo <file> --now`.259260### Scheduled execution — read-only only261262If a tool supports no hooks at all, a scheduled **detector** run (`repo_sync_check.py --quiet`, reports only, no audio flags) is acceptable — it's read-only and interruption-safe. Do **not** schedule `checkpoint_sync.py`: mutation stays event-driven (see design rationale above). The console tile's polling normally makes scheduled detection unnecessary.263264---265266## Relationship to Other Skills267268- **synthesis-mac-sync** — the full multi-machine sync operation (config files, credentials, all repos, with user approval). Repo-guard keeps same-machine work recoverable; mac-sync owns the explicit cross-machine publication transition.269- **synthesis-context-lifecycle / synthesis-daily-rituals** — those skills keep local project state current during work and publish it through remote handoff or day-end. `repo_sync_check.py` is the final day-end verification gate.270271---272273## Command Reference274275```276repo_sync_check.py [--workspace W] [--max-depth N] [--quiet] [--json]277 [--dirty-only] [--alert] [--speak] [--notify]278 [--report-dir D] [--no-report]279280checkpoint_sync.py [--config C] [--repo PATH] [--hook] [--now]281 [--flush-pending | --flush-session SESSION_ID282 [--drop-stranded --assert TEXT]]283 [--no-throttle] [--dry-run]284 [--prepare-worktree-retirement PATH285 --retirement-repository REPO --retirement-head SHA286 --retirement-remote REMOTE --retirement-base REMOTE_REF]287 [--complete-worktree-retirement INTENT]288 [--reconcile-retired-worktree PATH289 --retirement-repository REPO290 (--retirement-head SHA | --retirement-session SESSION_ID)291 --retirement-remote REMOTE --retirement-base REMOTE_REF]292 [--quiet] [--json]293 [--speak] [--notify]294```295296- `--speak/--notify/--alert` are generic + mute-aware on both scripts.297- `checkpoint_sync --repo` records one configured producer path locally; it does not commit or use the network.298- `--hook` consumes the calling session's JSON hook payload and never falls299 back to a workspace-wide mutation. `--flush-pending` is the explicit300 remote-context transition; `--no-throttle` is its console compatibility alias.301- `--flush-session` applies the same remote-readiness gates to one manifest302 selected by its exact session id. It does not read, validate, publish, or303 delete any other session manifest. Use it when unrelated pending work must304 remain recoverable while one fully published session transitions to305 `REMOTE_READY`.306- `--drop-stranded --assert TEXT` is valid only with `--flush-session`. It307 retires drop-eligible stranded entries after recording the operator's308 assertion in the retired-pending ledger; see "Stranded entries and309 per-repository retirement" above.310- `--retirement-session` derives a removed worktree's historical head from311 this native session's retained local receipt. It verifies the manifest312 digest, attributed bytes/deletions and file modes, and remote ancestry;313 it never substitutes current file existence for preservation evidence.314 Only that session's manifest is changed. The retirement transaction retains315 exact post-images for crash recovery and preserves evidence for remaining316 worktrees. Missing older evidence is an explicit recovery gap. See317 [checkpoint closure recovery](../synthesis-project-management/references/checkpoint-closure-recovery.md).318319---320321## Design Principles3223231. **Zero AI and zero external dependencies** — Python stdlib + git CLI (PyYAML used if present, minimal built-in parser otherwise)3242. **LLM-agnostic** — same scripts for Claude Code, Codex, Cursor, console, or manual use3253. **Detector never modifies; Stop is local-only; remote publication modifies only exact guarded context paths**3264. **Identifying names (repo, workspace, client) never on audio/banner surfaces** — counts and pointers only3275. **Remote mutation is explicit or day-end; only reads may poll**3286. **Fail closed, never force** — empty guard config disables; divergence/hook-failures stop and alert3297. **Composable** — exit codes, JSON output, shared state files330331## Changelog332333- **2.5.0 (2026-09-11):** classifies manifest entries beneath a removed,334 unrecorded worktree as `stranded` instead of aborting the whole manifest;335 adds `--flush-session ID --drop-stranded --assert TEXT` with an append-only336 retired-pending ledger; retires published repositories' entries per flush337 so blocked repositories no longer keep already-published work pending.338- **2.3.0 (2026-08-23):** adds a fail-closed exact-session remote handoff that339 retires one verified manifest without coupling it to unrelated pending340 sessions; preserves global flush behavior and fsyncs successful manifest341 retirement.342- **2.2.0 (2026-08-14):** makes retirement a lifecycle-locked, remote-pinned,343 fsynced, resumable transaction across manifests and receipts; resumes with344 the intent's exact content-addressed reconciler and compare-binds optional345 remote branch deletion to the verified head; keeps unexplained missing and346 unpublished paths fail-closed.347- **2.1.1 (2026-08-14):** clarifies that a clean no-edit task needs no empty receipt and that remote readiness compares complete branch heads rather than project-path history.348- **2.1.0 (2026-08-14):** separates local and remote readiness. Stop records atomic client-session receipts without Git or network mutation; interruption leaves a recoverable manifest; explicit remote handoff batches exact private-context paths only after source paths are upstream-current. Adds worktree identity, first-branch publication, generic commit messages, staged-index isolation, and integration fixtures.349- **2.0.0 (2026-07-08):** three-layer redesign. Generic-only audio/banner (confidentiality rule), `~/.synthesis/quiet-audio` mute flag, report files + history, remediation hints, new `checkpoint_sync.py` (event-driven auto-commit/push: runtime remote guard, quiescence, shared throttle, ff-only push, distinct author, stale-lock detection), synthesis-console integration contract, scheduled-mutation explicitly disallowed. Origin: 2026-07-08 design review (lesson: alert-channel confidentiality + event-driven checkpoints).350- **1.1.0:** detector + count-only audio alerts.