@ssheleg/agent-sync 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ # Claude Code hooks
2
+
3
+ **Read this when** installing, debugging or removing the enforcement hooks.
4
+
5
+ ## The limit, first
6
+
7
+ **Hooks exist only in Claude Code.** On Cursor, Codex and the other agents the
8
+ skills CLI serves there is no `PreToolUse`, so nothing blocks a guarded edit. On
9
+ those agents the same checks run as a self-check written into the skill body, and
10
+ the run is recorded on the board as `ungated`.
11
+
12
+ Never describe a project as protected when its agents run outside Claude Code. The
13
+ board's `gated` / `ungated` column exists precisely so that an operator can tell an
14
+ enforced run from a promised one.
15
+
16
+ ## Contract
17
+
18
+ Verified against the Claude Code hooks reference, 2026-07-29.
19
+
20
+ A `PreToolUse` hook blocks a call in either of two ways:
21
+
22
+ - **exit 2**, with the reason on **stderr** (stdout is ignored); or
23
+ - **exit 0** with this on stdout:
24
+
25
+ ```json
26
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse",
27
+ "permissionDecision":"deny",
28
+ "permissionDecisionReason":"agent-sync: docs/DECISIONS.md is held by run r-7f3a91"}}
29
+ ```
30
+
31
+ Any other exit code is a non-blocking error: execution continues and stderr is shown
32
+ in the transcript. So **a crashing guard fails open** — write the guard to exit 2 on
33
+ its own internal errors, or it silently stops guarding.
34
+
35
+ The hook receives JSON on stdin with `session_id`, `prompt_id`, `transcript_path`,
36
+ `cwd`, `permission_mode`, `hook_event_name`, `tool_name`, `tool_input` and
37
+ `tool_use_id`.
38
+
39
+ ## Installed hooks
40
+
41
+ ```json
42
+ {
43
+ "hooks": {
44
+ "SessionStart": [
45
+ { "matcher": "startup|resume",
46
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" }] }
47
+ ],
48
+ "PreToolUse": [
49
+ { "matcher": "Edit|Write|MultiEdit",
50
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh" }] },
51
+ { "matcher": "Bash", "if": "Bash(git commit *)",
52
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh" }] }
53
+ ],
54
+ "PostToolUse": [
55
+ { "matcher": "*",
56
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/renew.sh" }] }
57
+ ],
58
+ "SessionEnd": [
59
+ { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh" }] }
60
+ ]
61
+ }
62
+ }
63
+ ```
64
+
65
+ | Hook | Job |
66
+ |---|---|
67
+ | `session-start.sh` | Register the run, print the board summary and the one next action |
68
+ | `guard.sh` | Deny an edit to a `guardedFiles[]` path, or a commit staging one, without a live lease |
69
+ | `renew.sh` | Renew the lease, throttled to `renewIntervalSeconds` — a no-op most calls |
70
+ | `session-end.sh` | Release every lease, flush the journal, close the run |
71
+
72
+ ## Performance
73
+
74
+ `renew.sh` runs after **every** tool call. It must be a no-op in the common case:
75
+ it reads one timestamp file and returns. It touches the network at most once per
76
+ `renewIntervalSeconds` (default 300 s). If it ever becomes slower than that, the
77
+ throttle is broken — fix the throttle rather than removing the hook.
78
+
79
+ ## Debugging
80
+
81
+ | Symptom | Cause |
82
+ |---|---|
83
+ | Guarded edits go through | The guard crashed. Any exit code other than 2 is non-blocking. Run it by hand with a sample stdin payload |
84
+ | Everything is denied | No config, or no lease. `status` says which |
85
+ | Session start is slow | The backend is unreachable; it should time out at 5 s and degrade, not hang |
86
+ | Renew floods the log | The throttle file is not being written — check its path is writable |
87
+
88
+ Run the guard directly to see what it decides:
89
+
90
+ ```bash
91
+ echo '{"tool_name":"Edit","tool_input":{"file_path":"docs/DECISIONS.md"},"cwd":"'"$PWD"'"}' \
92
+ | "$CLAUDE_PLUGIN_ROOT/hooks/guard.sh"; echo "exit=$?"
93
+ ```
94
+
95
+ ## Removing them
96
+
97
+ Delete the `hooks` block from the project's `.claude/settings.json`. The skill keeps
98
+ working — every guard is also available as a command, and the board simply records
99
+ runs as `ungated` from then on.
@@ -0,0 +1,145 @@
1
+ # Lease and id-reservation protocol
2
+
3
+ **Read this when** changing acquisition, expiry, stealing, or id allocation — or
4
+ when two agents disagree about who holds something.
5
+
6
+ Two different mechanisms, and confusing them is how this went wrong twice:
7
+
8
+ > **A lease is decided by an atomic operation** — `O_EXCL` on one filesystem, or a pushed
9
+ > git ref across machines. **An id reservation is decided by replaying the log**, where
10
+ > allocation is positional and every reader computes the same answer.
11
+
12
+ The log never decides a lease. It records one, so other agents can see it.
13
+
14
+ ## Line grammar
15
+
16
+ One event per line, appended, never edited. Exactly this shape:
17
+
18
+ ```
19
+ - `2026-07-29T10:42:13Z` `op=acquire` `key=ASC-072` `run=r-7f3a91` `ttl=2700` `repo=account-session-connect` `sha=9bba6d2`
20
+ ```
21
+
22
+ Parsed by:
23
+
24
+ ```
25
+ ^[-*+] `(?P<ts>[^`]+)`(?P<pairs>(?: `[a-z_]+=[^`]*`)+)$
26
+ ```
27
+
28
+ **Emit `- `; accept `-`, `*` or `+`.** The bullet is deliberately liberal because a
29
+ knowledge base normalises markdown on the way in — Outline rewrites `- ` to `* ` —
30
+ so a parser anchored to the character you wrote rejects every line the server hands
31
+ back. Observed live, and it presented as a lost race rather than a parse failure.
32
+
33
+ Required on every line: `op`, `key`, `run`. `op` is one of
34
+ `acquire` · `release` · `renew` · `base` · `reserve` · `release_id` · `signal` · `journal`.
35
+
36
+ Unparseable lines are **counted and reported**, never guessed at. Anything
37
+ entry-shaped (`^[-*+] \``) that fails the full pattern counts as unparseable; blank
38
+ lines, prose and the generated marker are skipped without counting.
39
+
40
+ **Do not put a narrower pre-filter in front of the pattern.** A `continue` that
41
+ tests for the exact bullet you emitted skips malformed lines *before* they can be
42
+ counted, so the ratio reads 0% while nothing parses — the guard and the counter both
43
+ go quiet at once. This is not hypothetical; it is how the bug above stayed invisible.
44
+
45
+ **An unreadable log is not a lost race.** When more than 2% of a log fails to parse,
46
+ `acquire` **raises** instead of reporting `lost`, and the board gate fails. Reporting
47
+ a lost race would name a holder who does not exist and send the caller looking for
48
+ them.
49
+
50
+ ## Acquiring — the third design, and the first that is true
51
+
52
+ ```
53
+ 1. reap if .agent-sync/leases/<K>.lock exists and is expired, remove it
54
+ 2. create os.open(lock, O_CREAT | O_EXCL) — this is the decision, and it is atomic
55
+ 3. lost FileExistsError -> read the holder out of the file and report it
56
+ 4. won write {run, ts, ttl, repo}; publish op=acquire to the plane for visibility
57
+ ```
58
+
59
+ **Publishing is not the decision.** A failure to reach the knowledge base costs
60
+ visibility, never correctness: the lock is already held. So the append is wrapped and
61
+ its failure reported, not raised.
62
+
63
+ ### Why not the knowledge base
64
+
65
+ Two earlier designs were measured and rejected. **One shared append-only document** loses
66
+ writes: twelve concurrent appends, twelve reported successes, three lines present — so a
67
+ lease decided on it can be held by two runs, each with proof. **One document per writer**
68
+ loses nothing (12/12) but cannot decide: without compare-and-swap nothing knows whether a
69
+ contender is still writing, and eight parallel processes each read only their own shard —
70
+ **eight winners for one key**. A longer settle window reached five, never one.
71
+
72
+ `O_EXCL` answers what the store cannot: twelve processes, one winner, eleven losers naming
73
+ the same holder. Full history in `CHANGELOG.md` (1.0.0).
74
+
75
+ ### Cross-machine: `leaseBackend: "git"`
76
+
77
+ A lock file is exclusive between processes on **one filesystem**; two machines have two.
78
+ For cross-machine exclusion the lease is a commit pushed to
79
+ `refs/agent-sync/leases/<key>`, and the remote's non-fast-forward rejection **is** a
80
+ compare-and-swap — verified against a hosted remote, then proven with eight parallel
81
+ processes: one winner, seven losers naming it. Expired leases are stolen with
82
+ `--force-with-lease` against the exact object seen, so a steal cannot clobber a holder who
83
+ renewed in between.
84
+
85
+ The tool reports which guarantee is in force; it never implies the stronger one.
86
+
87
+ ## Expiry and stealing
88
+
89
+ A lock is expired when `now > ts + ttl` for the timestamp inside it, refreshed by
90
+ `renew`.
91
+
92
+ Default `ttl` is 2700 s (45 minutes). `renew` is emitted at most once per
93
+ `renewIntervalSeconds` (default 300 s) — by the `PostToolUse` hook in Claude Code,
94
+ and by the agent itself everywhere else.
95
+
96
+ **Stealing an expired lease is the ordinary `acquire` path.** There is no force flag:
97
+ the reap step removes an expired lock and the create proceeds. The steal is visible on
98
+ the plane with both run ids, so an operator can see that it happened and when.
99
+
100
+ ## Releasing
101
+
102
+ `release` on every path, including failure. An abandoned lease is indistinguishable
103
+ from active work until its TTL runs out, and during that window the task looks
104
+ taken. Report the failure and release; do not hold the lease "in case".
105
+
106
+ ## Id reservation
107
+
108
+ Reading a "next free id" line from a file is not reserving it. Allocation is
109
+ **positional over the log**, so no agent has to trust another's arithmetic.
110
+
111
+ A register is opened once:
112
+
113
+ ```
114
+ - `…` `op=base` `key=DEC` `value=0216` `run=r-bootstrap`
115
+ ```
116
+
117
+ Then, replaying in order and maintaining a free list:
118
+
119
+ - `op=release_id key=DEC value=NNNN` pushes `NNNN` onto the free list.
120
+ - `op=reserve key=DEC` takes the free-list head if it is non-empty; otherwise it
121
+ takes `base + (count of prior reserves not served from the free list)`.
122
+
123
+ Every reader computes the same assignment for every reserve line, including its own.
124
+
125
+ **An id you reserved and did not write to git must be released** with
126
+ `release_id`. An id that is reserved, unreleased and absent from git after its run
127
+ closes is reported by the board as a **leak** — never reclaimed automatically,
128
+ because a half-written decision on a branch is not the same thing as an unused
129
+ number, and silently handing it out again would produce two documents with one id.
130
+
131
+ ## The lease is not the claim
132
+
133
+ | Fact | Home | Lifetime |
134
+ |---|---|---|
135
+ | Who holds this task **right now** | the lease log | ephemeral, TTL |
136
+ | Who **owns** this task | the git claim tag (`[name]`, `todo (claimed: <role>)`) | durable |
137
+
138
+ `acquire` writes the git tag through and `release` restores exactly what was there. The
139
+ objection that once demoted this to a check — an unattended process rewriting a shared
140
+ registry is the collision a lease exists to prevent — is engineered out rather than
141
+ accepted: one row, one cell, refused on ambiguity, atomic, and reversible from stored
142
+ state. See `roadmap.md`.
143
+
144
+ Do not add a third place that records ownership — a project with two claim vocabularies
145
+ has, in practice, none.
@@ -0,0 +1,76 @@
1
+ # Binding to task-pipeline
2
+
3
+ **Read this when** wiring `pipeline.json`, or adding a stage hook.
4
+
5
+ `agent-sync` supplies stages; it does not define them. The stage names below are
6
+ `task-pipeline`'s own — do not rename, renumber or fork them.
7
+
8
+ ## Where it plugs in
9
+
10
+ | Stage | Calls | Why there and not elsewhere |
11
+ |---|---|---|
12
+ | **0 Intake grill** | `status`, then `acquire <KEY>` | The cloud KB and the board join the harvest's source ledger. The lease is taken **before the brief is committed**, or two agents write two briefs for one task |
13
+ | **1 Docs study** | — | External docs; nothing shared to coordinate |
14
+ | **2 Brainstorm + decompose** | `journal` | Also warns when a live run holds an overlapping key — cheapest moment to find the overlap |
15
+ | **3 Spec** | `reserve <REG>` per id | Ids must be reserved *before* they are written to git. Reading "next free id" is not reserving it |
16
+ | **4 Plan** | `journal` with the plan's file ownership | Parallel groups that write one file are a merge conflict scheduled for later |
17
+ | **5 Dev** | `renew` (automatic), `journal` | Also the owner of the submodule-commit → parent-gitlink bump. Nobody else has both repos in hand |
18
+ | **6 Tests** | `journal` | The suite result is evidence, and evidence belongs in the run |
19
+ | **7 Lint + deploy** | `journal` per gate | — |
20
+ | **8 Post-deploy** | `journal` | — |
21
+ | **9 Docs + wiki** | `signal` per dependency flip, then `board` | The main write point. The pipeline already updates docs here; the board is regenerated from what it wrote |
22
+ | **10 Acceptance** | `release` every lease, write the claim tag through | A run that ends without releasing looks alive until its TTL expires |
23
+
24
+ ## pipeline.json
25
+
26
+ `task-pipeline`'s `pipeline.schema.json` already permits this; nothing is forked.
27
+ Add `agent-sync` to `skills[]` on the six stages that call it:
28
+
29
+ ```json
30
+ {
31
+ "stages": [
32
+ { "id": "0", "title": "Intake grill", "skills": ["task-pipeline:grill", "agent-sync"],
33
+ "gate": { "type": "manual", "check": "brief committed and lease held" } },
34
+ { "id": "3", "title": "Spec", "skills": ["task-pipeline:spec", "agent-sync"],
35
+ "gate": { "type": "auto", "check": "every id in the spec was reserved" } },
36
+ { "id": "4", "title": "Plan", "skills": ["task-pipeline:planning", "agent-sync"],
37
+ "gate": { "type": "auto", "check": "no two parallel tasks write one file" } },
38
+ { "id": "5", "title": "Dev", "skills": ["task-pipeline:build", "agent-sync"],
39
+ "gate": { "type": "auto", "check": "lease live and submodule pointers current" } },
40
+ { "id": "9", "title": "Docs + wiki", "skills": ["task-pipeline:artifacts", "agent-sync"],
41
+ "gate": { "type": "auto", "check": "board regenerated and no mirror drift" } },
42
+ { "id": "10", "title": "Acceptance", "skills": ["task-pipeline:acceptance", "agent-sync"],
43
+ "gate": { "type": "auto", "check": "every lease released and every claim tag written through" } }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ Stages 1, 2, 6, 7 and 8 keep their own `skills[]`; `agent-sync` only journals there,
49
+ which needs no wiring.
50
+
51
+ ## Preflight
52
+
53
+ `task-pipeline` is required. When it is absent, print the install line and **stop** —
54
+ do not improvise a substitute flow, because without those stages there is nothing to
55
+ bind to and the result is ad-hoc work wearing a pipeline's vocabulary.
56
+
57
+ ```bash
58
+ npx sshlg-skills install
59
+ ```
60
+
61
+ That installer also brings `super-ux` (its stage-3 UX track is required for
62
+ user-facing work) and `make-skill`, and it prunes the duplicate plain-copy shadow in
63
+ `~/.claude/skills/` that otherwise serves a stale skill over the installed plugin.
64
+
65
+ ## Gate expressions
66
+
67
+ Each `check` above is verified by the coordinator, not by prose:
68
+
69
+ | Check | How it is decided |
70
+ |---|---|
71
+ | lease held | replay the log; holder == this run |
72
+ | every id reserved | every `DEC-`/`OQ-`/`DEP-`-shaped token new in the diff has a `reserve` line in this run |
73
+ | no two parallel tasks write one file | intersect the file lists journaled at stage 4 |
74
+ | submodule pointers current | `git submodule status` reports no `+` prefix |
75
+ | board regenerated, no drift | each mirror stamp equals `git rev-parse HEAD` for its source |
76
+ | every lease released | replay the log; this run holds nothing |
@@ -0,0 +1,103 @@
1
+ # Working the roadmap: claims, closing, re-planning
2
+
3
+ **Read this when** configuring `claimTags`, taking or releasing a task, closing one, or
4
+ re-planning work that is already on a board.
5
+
6
+ The roadmap is where a project says *what is being done and by whom*. With several
7
+ agents it is also the file most likely to be written by two of them at once, which is why
8
+ it is guarded — and why the tool touches it as narrowly as it possibly can.
9
+
10
+ ## The two records, and why both exist
11
+
12
+ | Record | Says | Lives | Lifetime |
13
+ |---|---|---|---|
14
+ | **Lease** | who holds this task *this minute* | the lease backend | expires by TTL |
15
+ | **Claim tag** | who *owns* this task | the roadmap, in git | until released or done |
16
+
17
+ A lease without a claim tag is invisible to anyone reading the repository. A claim tag
18
+ without a lease is a name that never expires — the stale `[Backend]` that blocks a task
19
+ for a week because someone's session died. **Both, or neither.**
20
+
21
+ `acquire` writes the tag through; `release` restores exactly what was there. You do not
22
+ write it by hand, and you should not: an editor rewriting a shared registry is the
23
+ collision the lease exists to prevent.
24
+
25
+ ## How the write is made safe
26
+
27
+ The roadmap is a shared registry, so the edit is deliberately the smallest one possible:
28
+
29
+ 1. **One row.** The tool finds the *single* markdown table row containing the task id as
30
+ a whole word. **Zero rows → nothing happens. Two or more → it refuses and says so.**
31
+ It never guesses which row you meant.
32
+ 2. **One cell.** Only the configured cell of that row changes. Everything else on the
33
+ line — links, notes, other columns — is untouched, byte for byte.
34
+ 3. **Reversible.** The previous cell text is stored in `.agent-sync/claims.json` and
35
+ restored verbatim on release. Not a default, not a guess: what was actually there.
36
+ 4. **Atomic.** Written to a temporary file and moved into place, so a crash mid-write
37
+ cannot leave the register half-edited.
38
+
39
+ After `acquire` then `release`, `git diff` on the roadmap is **empty**. If it is not,
40
+ that is a bug, not a convention.
41
+
42
+ ## Configuring it
43
+
44
+ ```json
45
+ "claimTags": {
46
+ "docs/WORKSTREAMS.md": { "mode": "cell", "cell": 2, "held": "{prev} · claimed {holder}" },
47
+ "apps/*/docs/ROADMAP.md": { "mode": "cell", "cell": -1, "held": "{prev} (claimed: {holder})" }
48
+ }
49
+ ```
50
+
51
+ - `cell` is **0-based** over the row's cells. `-1` is the last cell, `-2` the one before
52
+ it. Getting this wrong writes the claim into the wrong column and looks like it worked —
53
+ check the result of your first `acquire` against the file before trusting the mapping.
54
+ - `held` is a template. `{prev}` is the cell's current text, `{holder}` the run id.
55
+ Keeping `{prev}` is what makes `todo` become `todo (claimed: r-x)` rather than losing
56
+ the status.
57
+ - A pattern may cover many files (`apps/*/docs/ROADMAP.md`); each is searched
58
+ independently, and a file with no matching row is simply skipped.
59
+
60
+ `check` validates that every pattern matches a real file and that the cell exists.
61
+
62
+ ## Closing a task
63
+
64
+ Closing is **not** the same as releasing, and the tool does not do it for you.
65
+
66
+ ```
67
+ release <KEY> → the claim is removed; the row returns to what it said before
68
+ ```
69
+
70
+ That is the right behaviour when you stop working. **Closing** means the row's status
71
+ becomes `done` — a statement about the work, not about who was holding it, and one only
72
+ you can make. Do it as part of the same change that lands the work:
73
+
74
+ 1. `record` what you actually built, with the decision id and the files;
75
+ 2. edit the row's status to `done` yourself — you hold the lease, so the guard allows it;
76
+ 3. `release` the lease;
77
+ 4. `reconcile`, then `board`.
78
+
79
+ The tool refuses to write `done` on your behalf for the same reason it refuses to guess a
80
+ row: a status that a machine sets is a status nobody checked.
81
+
82
+ ## Re-planning
83
+
84
+ New tasks, split tasks, re-ordered phases — all of it is an ordinary guarded edit:
85
+
86
+ - **Take a lease on the board itself** before restructuring it. Use the direction or
87
+ board id as the key (`WS-9`), not a task id, so the lease says what it means.
88
+ - **Reserve every new id** with `reserve` before writing it. Re-planning is exactly when
89
+ several agents mint ids at once, and reading a *next free id* line is not reserving it.
90
+ - **A task that moves keeps its id.** Ids are stable; renumbering breaks every reference
91
+ in every other document and in the as-built record.
92
+ - **Record the re-plan as a decision** if it changes scope. A board that quietly grew a
93
+ phase is a scope change nobody agreed to.
94
+
95
+ ## When the claim cannot be written
96
+
97
+ If `acquire` prints nothing about the claim, no row matched — the task id is not on the
98
+ board, or not in a table row. That is worth noticing rather than ignoring: it usually
99
+ means the key you leased is not the key the board uses.
100
+
101
+ If it prints *refusing to guess which one*, two rows carry the id. Fix the board or
102
+ narrow the pattern; do not work around it by editing by hand, because the next agent's
103
+ lease will hit the same ambiguity.
@@ -0,0 +1,131 @@
1
+ # Two documentation sources, and the duty to reconcile them
2
+
3
+ **Read this when** starting a task, finishing one, or deciding where a piece of
4
+ documentation belongs.
5
+
6
+ ## They answer different questions
7
+
8
+ | Source | Answers | Written | Authority over |
9
+ |---|---|---|---|
10
+ | **Git docs** (`docs/`, ADRs, specs, contracts) | *How it should be* | before the code, often without it | intent, design, scope, decisions |
11
+ | **The coordination plane** (`70 As-built`) | *How it actually is* | from what agents really wrote | the record of what was built, by whom, at which commit |
12
+
13
+ Neither is a copy of the other and neither outranks the other, because neither is
14
+ answering the other's question. A spec that describes an unbuilt thing is not wrong —
15
+ it is intent. An as-built record that contradicts the spec is not wrong either — it is
16
+ what happened.
17
+
18
+ **The gap between them is the finding.** It is drift between plan and reality, and
19
+ surfacing it is the whole point. A system where the two can never disagree has simply
20
+ hidden the disagreement.
21
+
22
+ This is why the as-built record does **not** violate a single-source-of-truth rule:
23
+ there is no second home for one fact, there are two facts. What would violate it is
24
+ copying a decision's *body* into the cloud and editing it there.
25
+
26
+ ## The duty, both ends of a task
27
+
28
+ **Before starting** — during the pipeline's docs-study stage:
29
+
30
+ ```bash
31
+ python3 "$SKILL_DIR/scripts/agent_sync.py" reconcile
32
+ ```
33
+
34
+ Read the git documents for the area you are about to touch **and** the as-built
35
+ record for it. Then resolve every divergence one of three ways:
36
+
37
+ 1. the git document is stale → fix it, or record why it stands;
38
+ 2. the as-built record is wrong or incomplete → correct it with `record`;
39
+ 3. they genuinely disagree and the disagreement is real → that is a decision to make,
40
+ not a discrepancy to paper over. Raise it before you write code against either.
41
+
42
+ Starting work on top of an unresolved divergence means building against a document
43
+ that describes a system that does not exist.
44
+
45
+ **After finishing** — during the pipeline's docs stage:
46
+
47
+ ```bash
48
+ python3 "$SKILL_DIR/scripts/agent_sync.py" record "what you actually built" --decision DEC-0216 --files a.py,b.ts
49
+ python3 "$SKILL_DIR/scripts/agent_sync.py" reconcile
50
+ ```
51
+
52
+ Update **both** sides in the same change: the git documents that state intent, and the
53
+ as-built record of what landed. Then run the check again. A task that updated only one
54
+ side has left the next agent a divergence to discover the hard way.
55
+
56
+ ## What `reconcile` decides, and what it refuses to
57
+
58
+ It is mechanical, and it says so in its own output. It finds:
59
+
60
+ - an as-built entry whose commit is **not in this repository's history** — recorded
61
+ from a branch that never landed, or from another repo;
62
+ - an id written in a register **after the baseline** with no as-built record — decided
63
+ since adoption, and nothing reports it was built;
64
+ - an as-built entry citing an id that **exists in no register** — built against
65
+ something never written down.
66
+
67
+ It does **not** judge whether the built thing matches what the document describes.
68
+ That is a reading, not a diff. The tool points at where to look and refuses to imply
69
+ it checked the substance.
70
+
71
+ ## The baseline — why the check is a ratchet
72
+
73
+ A project adopting this on day one has every prior decision unrecorded. A check that
74
+ reports all of them reports nothing: it is noise, and noise is what gets a gate
75
+ switched off.
76
+
77
+ ```bash
78
+ python3 "$SKILL_DIR/scripts/agent_sync.py" reconcile --set-baseline
79
+ ```
80
+
81
+ Run once per project. Ids at or below the baseline become a **backlog** — counted,
82
+ visible, allowed only to shrink. Ids after it must carry an as-built record or the
83
+ check fails. This is the same shape as a well-behaved lint ratchet, and for the same
84
+ reason: a gate that fails on history is a gate nobody keeps.
85
+
86
+ ## Where a piece of documentation belongs
87
+
88
+ | It is… | Home |
89
+ |---|---|
90
+ | a decision about what to build | git — the decision register |
91
+ | a contract, schema, spec | git |
92
+ | user-facing behaviour | git |
93
+ | "this is what I implemented, here is the commit" | the as-built record |
94
+ | "the implementation diverged from the spec, here is why" | **both** — as-built for the fact, git for the decision |
95
+ | who is doing what right now | the claims log — ephemeral, never git |
96
+
97
+ When in doubt: if it must survive the tool being uninstalled, it goes in git.
98
+
99
+ ## Lifetime, and why nothing is deleted
100
+
101
+ | Information | Home | Lifetime |
102
+ |---|---|---|
103
+ | Decisions, specs, contracts, user-facing behaviour | git | permanent, append-only register |
104
+ | What was actually built, with its commit | as-built log | permanent, append-only |
105
+ | Cross-repo dependency state | signal log | permanent, append-only |
106
+ | Who holds a task right now | claims log | expires by TTL |
107
+ | Per-run narrative | that run's journal | permanent |
108
+ | The board, the repo page, the setup snapshot | generated | replaced on every regeneration |
109
+
110
+ **No log entry is ever edited or deleted.** The logs are replayed in order to decide who
111
+ holds what and which id was allocated, so removing a line silently rewrites a conclusion
112
+ every other agent has already acted on. Correct by **appending**:
113
+
114
+ - a lease is **released**, never removed;
115
+ - a reserved id you did not use is returned with `release-id`, which appends;
116
+ - a wrong as-built entry is superseded by a later, correct one, and both stay visible —
117
+ the history of what was believed is itself worth keeping.
118
+
119
+ Generated pages are the exception, and a narrow one: they are rewritten wholesale, and a
120
+ page whose first line has lost its `agent-sync:generated` marker is **refused** rather
121
+ than overwritten, because a human took it over.
122
+
123
+ **Growth.** These logs are small — one line per event — so rotation is not urgent. When a
124
+ log does need trimming, archive the whole document and start a fresh one with a `base`
125
+ line carrying the current allocation state. Never delete lines from a live log to shrink
126
+ it: replay would then produce a different answer than it did yesterday.
127
+
128
+ **Removing the tool.** Everything durable is already in git. Delete `.agent-sync/`, the
129
+ config and the env file; the knowledge-base pages can be kept as a record or archived.
130
+ Nothing in the repository depends on the tool being installed, which is the property that
131
+ makes adopting it reversible.