Skip to content

Releases: ssheleg/agent-sync

v1.23.4 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 28 Oct 04:24
1b36ef4
  • The guard fails silent on an Edit payload of the wrong shape (hub B-211 follow-up): a payload
    that is not an object, a tool_input that is a string, or a file_path that is not a string
    raised inside the parser, and the parser's non-zero exit failed CLOSED (exit 2) in a configured
    session. Such a payload names no file and now passes, as the apply_patch branch already did.
    Three GUARD_SHAPES rows; two of them fail on 1.23.3.
  • The generated AGENT_SYNC.md carries no em dash (hub B-289): the setup snapshot's headings,
    table cells, wiring and credential lines, and the generated-file marker use a colon, semicolon
    or parentheses. The marker keeps its agent-sync:generated source=... cfg=... at=... fields;
    a project's snapshot changes on its next agent_sync.py setup.

v1.23.3 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 27 Oct 15:18
6a30f41
  • Codex edits are guarded (hub board row B-211). Codex sends every file edit as one tool,
    apply_patch, with the patch text in tool_input.command and no file_path. The PreToolUse
    matcher did not name it, and an empty path fell into the Bash branch, which parsed the patch as a
    shell command: a guarded file edited through a patch passed, and a patch whose body held
    > DECISIONS.md or git commit was refused. The matcher now names apply_patch; guard.sh
    reads every *** Add File:, *** Update File:, *** Delete File: and *** Move to: path from
    the patch (relative paths against the payload's cwd), checks each like an Edit, and never hands
    the patch to the Bash branch. A payload that does not parse names no path and passes silently.
    Twelve apply_patch rows in GUARD_SHAPES (test/validate.py); eight of them fail on 1.23.2.
  • Board: AS-23 and AS-24 are closed with their receipt (PR #35, 8c93030, released in v1.22.2);
    their status had read "merge pending" since the merge.

v1.23.2 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 26 Oct 00:11
f3828c5
  • Description within a 300-character budget (hub board row B-255 (a)): Claude Code lists every
    skill's description in one ~30,000-character budget, and on the operator's machine the listing
    stood at 51,178. The description falls from 447 to 277 characters (hub measure); the phrases it
    dropped (the submodule and parent-pointer pair among them) move to a new ## Triggers section,
    where the family routing hook still finds them.

v1.23.1 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 24 Oct 02:56
7c3f18a
  • test/family_gate.py, the family's canonical member gate (hub B-108, B-109): the board
    parses (every row as wide as its header, no id twice) and release.yml keeps the tag-ancestry
    check (fetch-depth: 0, git merge-base --is-ancestor), each watched failing on a plant by
    --self-test. Wired into the gate; a byte-for-byte copy of the hub's member-gate/family_gate.py.

v1.23.0 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 20 Oct 06:15
5c169af

SessionStart says only what this run must act on (AS-25). The hook printed the full
status at every startup, resume and /clear: 834 characters in the hub checkout
(measured 2026-10-20), mostly the setup and other runs' residue, closing on "NEXT: acquire
a lease", which the family's routing block already says and a read-only session never
needs. It now runs status --brief: one line naming the leases this run holds, a lease
held past scheduledIntervalSeconds, this run's own expired locks (with reap) and claim
tags out of step with the leases, or nothing at all. The brief reads local state only (no
record plane, no remote, no vault) and leaves the signal watermark alone, so the next
status still shows what is new. It prints on startup only; resume and clear still
stamp the session (the matcher keeps both, ASY-08) and print nothing, without starting
python. AGENT_SYNC_SESSION_VERBOSE=1 restores the full block; status is unchanged.
Measured interleaved on this machine at load average ~470: startup 834 to 0 characters,
median 1037 to 533 ms (p90 1399 to 853 ms, n=15 each); resume 920 to 63 ms median.

The PreToolUse guard answers inside its budget (AS-26). In git lease mode the first
write of a guarded file took that file's claim on the lease remote, synchronously: one or
two ls-remote calls (1.6 s each to github.com) and a push with no timeout, plus a cloud
record-plane append where one is configured. Measured against github.com, the 1.22.2 guard
took 8.6, 8.9 and 10.9 s for three first writes; the 2026-10-20 audit saw 10 to 20 s
against the hook's 20 s timeout, past which the host lets the call through anyway. The
claim now runs in a forked, detached process and the guard waits for it only until
AGENT_SYNC_GUARD_BUDGET (1.5 s from the coordinator's start by default; 0 restores the
synchronous claim). Past the budget the write proceeds under the task lease; the claim
then lands and leaves this run's local note, or records the run that holds it, and the
next write of that file is refused from that record without a round-trip (rechecked after
60 s). A second write of the same file while the claim is in flight waits for that claim
instead of racing it, and a claim this run already won is never reported as another
run's. The acquire push is bounded at 60 s. Local leases on a local record plane are
unchanged: they are a file create and are never backgrounded. Same three files on the
same remote with 1.23.0: 4.0, 5.2 and 4.0 s, with the remaining time spent in local file
writes on a machine at load average ~500 (two 50-byte writes profiled at 1.2 s).

The skill description is a router again (AS-27): 445 characters, down from 936; every
trigger the hub routes on (lib/triggers.js, route agent-sync) is still in it, in
English and Russian, with the boundary and the refusal phrase. The "before editing a
shared registry, even unasked" rule moved into the body.

  • Tests: test/hooks_session_test.py gains 8 cases (7 watched failing on 5bd8bec);
    new test/guard_budget_test.py, 5 cases over a local bare remote with a git shim that
    slows ls-remote and push by 5 s (3 watched failing on 5bd8bec: guard calls of
    11.06 s, 5.51 s and 11.36 s). Both run in npm test and CI.
  • Not covered: the window between a write that proceeds and its claim landing; a run that
    writes a file another run claimed in that window is refused only from its next write.
    Trigger evals were not re-measured for the new description (backlog AS-28).

v1.22.2 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 13 Oct 04:24
3dbafd8
  • Automatic heartbeats renew only claims with recent guarded activity or explicit
    acquisition/renewal. Unrelated tool calls no longer extend every task indefinitely.
    Manual renewal remains intentional. The activity window is two renewal intervals;
    the configured lease TTL still controls exclusion from the last successful renewal.
  • A guard requires a live task and durable activity/binding metadata. Missing task
    ownership or an unwritable binding refuses admission while retaining the claim for
    explicit repair. Expired ambiguous task associations are cleared, not guessed.
  • Releasing a handed-off task releases its associated file claims without freeing
    another task's files. Incomplete authority release remains retryable; activity-file
    cleanup after confirmed release is best-effort and cannot resurrect a lease.
  • Document prompt release after a pushed PR handoff and distinguish dependency
    signal state from a cross-project board ask. Existing lease keys, arbitration,
    generations and wire payloads are unchanged. Old installed heartbeats retain their
    old behavior until upgraded.
  • Verification: 19 real CLI/fake-clock controls, including same-second metadata
    failures and a local bare Git remote. The source candidate passed 20 suites and
    all 75 existing negative plants before this synchronized packaging bump.

v1.22.1 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 12 Oct 18:50
0e0143a

Patch from a re-certification reading of 1.22.0 (517b4b6).

A PR body that mentions git commit is no longer a commit. When a Bash command does not
tokenise (an odd-quote heredoc), 1.22.0's AS-18 fallback in bash_writes.py counted the
payload as a commit if git … commit appeared ANYWHERE in it — so gh pr create --body "$(cat <<'EOF' … every git commit … a 12" display … EOF)" with a guarded file staged and no
lease was refused with "this commit … stages a guarded file". The fallback now reads only
command text: backslash-newline continuations joined, each balanced $(…) and backtick span
removed (quote-aware, nested), each heredoc body removed (from the <<DELIM / <<'DELIM' /
<<-DELIM line through the line that is exactly DELIM) and comments dropped; an opener that
never closes ends the text there. In what remains, split into segments outside quotes, a
commit counts only where a segment starts with git … commit (after env assignments, the
usual prefixes and git -C <dir> / -c k=v).

The fallback checks the repository the commit runs in. git -C <dir> commit … and
cd <dir> && git commit … now name that repository's index, not the payload's cwd. A
directory it cannot name (cd -, cd "$(…)", a $VAR, a path that does not exist) falls
back to cwd, as guard.sh already did for a repo that is not a directory.

  • test/coordination_test.py: case "the untokenisable commit fallback reads only a git
    commit command" — 52 cases. Against 517b4b6, watched failing: the PR body exits 2
    (want 0); git -C <other> commit with the guarded file staged only in cwd exits 2 (want 0)
    and with it staged only in <other> exits 0 (want 2); cd <other> && git commit the same
    (0, want 2). The existing odd-quote git commit -m "$(cat <<'EOF'…)" case stays exit 2.
  • Blind reading of the first candidate (ea936ae, PR #33) failed it: it cut the command at
    the FIRST $( / <<, so a commit after a leading substitution was missed — main refused
    these, the candidate passed them. Added, each watched exiting 0 on ea936ae (want 2):
    cd "$(git rev-parse --show-toplevel)" && git commit -m <odd heredoc>,
    MSG="$(cat <<'EOF' … it's "x … EOF)" && git commit -m "$MSG",
    git add "$(git ls-files -m)" && git commit -m <odd heredoc> (at the parser,
    (False, None) on ea936ae, now a commit in cwd), and git \<newline> commit -m <odd heredoc> (continuation). Regression guards that pass on both: echo `date` && git commit -m <odd heredoc> exits 2; a heredoc body saying git commit is not a commit.
  • references/hooks.md "A commit" states the fallback; guard.sh's capability matrix has
    a row for an untokenisable commit. README's scheduledIntervalSeconds row and the
    ledger's S9 row describe the held-too-long notice as one JSON object: systemMessage for
    the person, hookSpecificOutput.additionalContext for the model.
  • Not covered: what an untokenisable command stages before its commit (only the index is
    read); a ) in a case pattern inside $(…) closes the span early; a heredoc opened
    inside a $(…) that closes on the same line keeps its body as command text.

v1.22.0 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 12 Oct 12:50
517b4b6

Integration wave 3 of the family's 2026-10-11 waves run (hub plan R17–R22), and two fix
rounds after blind verifiers failed the first candidate (27f485a) and the second
(e32c7c9, PR #32). test/coordination_test.py joins npm test with 50 cases. Against
1.21.7, 30 of the first 33 fail (13 on an assertion, 17 because the code they call does not
exist there) and 3 pass — "an id really missing is still a leak", "a short key keeps its
readable id" and "the same key is the same run every time" are regression guards. Against
the first candidate 27f485a, the 14 cases added in the first fix round all fail, as do the
extended parser case and the long-expired case (its message changed from > 7d to >= 7d).
Against the second candidate e32c7c9, the 17 cases added in the second fix round all fail
(11 on an assertion, 6 with an exception — three call a function that does not exist there, one a function whose signature changed, one hits the very ImportError it pins, one parses the hook's empty output), as do the
extended parser case and the vault-cache case.

npm test now runs every CI test suite, sandboxed. It runs test/run_gate.py: every suite
CI runs — the five it ran before, every test/audit_regressions/*.py (CI ran them and
npm test did not, so CI went red on FIX-SY-04.02 behind a green local gate) and the
installer test — each with its own temporary HOME and without CLAUDE_CODE_SESSION_ID,
CLAUDE_SESSION_ID or AGENT_SYNC_RUN_ID, then checks that the HOME it was started with has
the same ~/.agent-sync files as before. A test run had written 14 files into the
operator's real ~/.agent-sync/session-repos: claim_cell_test.py's cli() passed the
real HOME and the live session id through, and SESSION_REPOS_DIR was resolved at import.
Every suite now calls test/_sandbox.py first (a temp HOME that also holds its mkdtemp
fixtures, removed at exit), session_repos_dir() reads HOME at each call, and the case
"the suite never writes the HOME it was given" fails without the sandbox (watched: a
sentinel HOME received 3 files). The other direction too: CI's validate job had never run
guard_uncoordinated_test.py or coordination_test.py, which npm test did; it runs both
now, so the two gates run the same set.

Fix round 2 (second blind reading of e32c7c9)

  • SKILL.md states the two write modes again (FIX-SY-04.02, CI red). The compaction
    had dropped the paragraph its audit regression pins: a task lease authorizes the TASK,
    never the file; the resource claim is the short transaction lock; heavy overlap takes an
    isolated worktree + merge; the guard does NOT promise enforcement from a single task
    owner. Body 4726/4750 tokens (the $SKILL_DIR paragraph and the interval line trimmed).
  • git add X && git commit in one call is guarded (AS-16). The hook reads the index
    before the command runs, so X was not staged yet and the commit passed. The commit parser
    moved into bash_writes.py --commit (the same tokeniser as the write parse) and reads
    what the same command stages — git add <specs>, -A, -u, ., git commit -a,
    git commit <paths> — asking git ls-files which files that is; those are checked with
    the index in ONE coordinator call.
  • status names a token read from the file as degraded (unit B1). status loaded the
    file, then Sync() loaded it again, found the token in the environment and relabelled it
    "from the environment". The source is now assigned once per process.
  • The vault is asked once per process, and only where a token is used. The decision is
    memoised per root before any child starts (risk b). renew and release --held no longer
    ask at all (seam S2/S3): the lease they act on is a local lock or a git ref, and what the
    token would add is one visibility line in the Notion claims log, whose readers apply the
    TTL anyway — while renew.sh runs after every tool call (two use audit rows per call
    once a slot existed) and SessionEnd has 2 s, which a probe and a re-run consumed. With the
    token in the plaintext file they still publish. A re-run keeps the hook's clock: the first
    process records its start in AGENT_SYNC_HOOK_T0 before execv (risk a). The run probe
    is cached beside the slot answer (a pass for the hour, a refusal for a minute), and a
    missing slot is asked again after a minute even with no file token (risk g).
  • A host without fcntl no longer crashes acquire after the lease was won (seam S4).
  • The guard is cheaper and gated on the repository that owns the write (seam S5/S6/S8).
    bash_writes.py --guarded resolves each target's repository by walking to .git, drops
    every target no guardedFiles pattern matches, and the hook runs the coordinator once per
    owning repository with all its targets; guard memoises its read-only git questions; a
    Bash call spawns no interpreter until its payload carries a write shape (a >, or
    tee/sed/cp/mv as a WORD — used and guarantee no longer match) or commit; hook
    bytecode is cached under ~/.cache/agent-sync/pycache instead of recompiled. Because
    the owner decides, a session rooted outside every coordinated repository that writes into
    one is refused at the write. renew.sh decides without reading stdin wherever it can and
    reads only the payload's first 4 KB otherwise. Measured interleaved against e32c7c9
    under load average ~45 (absolute numbers on this machine are noisy; the spawn counts are
    not): a coordinated two-target redirect with the lease held 244 ms (git 2, python3 3) vs
    477 ms (git 10, python3 4); renew.sh on a 2 MB PostToolUse payload in an uncoordinated
    repository 8.1 ms vs 89.1 ms (medians of 15).
  • mv writes its source too (seam S7), and the parser reads <<\EOF, <( … ) and
    >( … ), cd -L/-P, timeout/nice/stdbuf/xargs prefixes, sed --expression=,
    env -C DIR, a cd in a pipeline or behind & (no move), <> (a write), and an absolute
    cp/mv destination directory whatever the current directory is (risk d). What it still
    skips is listed as UNPARSED in guard.sh and the module docstring. Its main() prefilter
    no longer misses sed 's/;/x/' -i f (risk c).
  • A lease held too long is said where the session sees it (seam S9). R19's warning
    printed only from status at SessionStart, when a fresh session holds nothing. The guard
    now emits it once per lease per run, as one JSON object with systemMessage (the person)
    and PreToolUse additionalContext (the model) — see the final polish below.
  • Smaller corrections. init --backend outline no longer says set -a && . ./.env.agent-sync
    (every command and hook reads the file itself); the generated wiring snapshot names the
    vault slot first for notion, the file only as a degraded fallback; check enforces the
    schema's scheduledIntervalSeconds minimum of 60; a lease ref is flagged keyless only when
    its payload parses as a JSON object without key, and the warning says it "was written by
    ≤1.21.2 (ts …)" — a date, not proof of a live writer; citations of project-observatory
    DECISIONS.md corrected to :2023 and :12873.

Final polish (third blind reading of 6ab4c47)

  • git commit --gpg-sign <path> is guarded. The AS-16 parser took --gpg-sign as an
    option with a value, so the pathspec after it was swallowed and the commit passed with a
    guarded file unleased; the key id attaches only as --gpg-sign=<keyid>. Case added to
    "git add and commit in one call is guarded" (it fails on 6ab4c47: the plan staged nothing).
  • A heredoc commit message with an odd number of quotes is still a commit (AS-18). The
    tokenizer closes a "…" string at the first inner quote, so such a message did not parse
    and the commit check was skipped, staged guarded files included (1.21.x skipped every
    heredoc commit). It is now checked against the index of the payload's directory. Case
    added; on 6ab4c47 commit_plan answered "not a commit".
  • The held-too-long notice reaches the model, once (AS-21). Claude Code shows a
    systemMessage to the person only, and it reads two JSON lines on stdout as a parse
    failure that drops both — which a Bash write into two repositories, each with a lease held
    too long, printed. The guard now prints one object with systemMessage and
    hookSpecificOutput.additionalContext, merging several repositories' notices. Cases "a
    lease held too long is said once where the session sees it" (extended) and "a write into
    two repositories says both notices in one object" — both failed on 7009bb9.
  • main() starts clean. The git answer memo, the token source and the vault-degraded
    note are reset per invocation, so a second in-process call never sees the first's.
  • Docs. fabric-coordination.md names the real flag (reserve <register> --key <k>,
    not --rkey); this entry's feature list sits under its own heading. npm test runs every
    CI test suite: the validator self-test, the coordinator smoke test, the stray-SKILL.md
    check, claude plugin validate --strict and the skill-audit job stay CI-only.

Upgrading to 1.22.0 mid-flight

  • A session's run id changes. Claude Code exports CLAUDE_CODE_SESSION_ID to every Bash
    call, and 1.22.0 reads it: a session whose Bash calls used the shared identity under
    1.21.x now resolves to its own id, so a lease it took under the shared id looks like
    another run's. Release before upgrading, or let it expire. 1.22.0 does NOT adopt the
    shared entry's id on first sight of a session key — that would merge this session with
    whichever session last wrote the shared entry, the collision B-150 removed.
  • The session list moved. 1.22.0 keys the list of other repositories by the session
    (~/.agent-sync/session-repos/<sha256>.json); a list keyed by project slug from a
    pre-release build is not read. Leases listed only there are renewed and released by hand.
  • **Observatory's tick cha...
Read more

v1.21.7 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 11 Oct 04:30
210ce1b
  • acquire refuses the shared fallback identity where hooks know their session. From
    a shell with no CLAUDE_SESSION_ID, acquire won the lease under the shared fallback,
    while the PreToolUse guard — which runs with the session id — refused every edit with
    "this run holds no lease" (reported 2026-10-11 from a linked worktree). When any session
    stamp is in reach, acquire now exits 2 and names the remedy (CLAUDE_SESSION_ID, or
    AGENT_SYNC_RUN_ID for a non-Claude caller). Where no hook stamps sessions, nothing
    changes: CLI and hook share the fallback and agree.
  • A linked worktree reads the main checkout's session stamp. A session started in the
    main checkout stamped that checkout's state dir; the walk from a worktree read only its
    own and fell back to the shared identity. The walk now reads both.

Two cases in test/hooks_session_test.py, both watched failing first.

  • A direct guard <path> in a repository with no coordination allows. guard.sh
    already exited early there, but agent_sync.py guard <path> called directly still
    exited 2 with a lease message no lease could satisfy. Recovered from the unmerged
    2026-09-11 branch: a missing config is NotCoordinated, an answer the guard allows;
    an unreadable config still denies. test/guard_uncoordinated_test.py (4 cases, 2
    failing on 1.21.6) joins npm test.

v1.21.6 — agent-sync

Choose a tag to compare

@github-actions github-actions released this 10 Oct 19:23
d23633d
  • npm test no longer runs the validator's self-test. The default gate ran
    python3 test/validate.py --self-test — 74 planted defects, each a full validator run in
    its own copy of the tree — and locally it did not finish in 58 minutes (measured
    2026-10-10). The self-test moves to npm run test:negatives, as in the sibling members;
    npm run test:all runs both. Nothing CI enforced moved: validate.yml and release.yml
    already invoke validate.py --self-test directly, not through npm test.
  • The release refuses a lightweight tag. A new step right after checkout in
    release.yml fails unless git cat-file -t <tag> is tag. git describe and
    git submodule status see annotated tags only; on 2026-10-09 four family members cut
    lightweight tags and the hub read each as its previous release. Same step, same wording,
    in every member.
  • Not a code change: ssheleg/sshlg-skills#154 (reported against 1.21.0) was fixed in
    1.21.2 — the guard asks the repository that owns the write. Its reproduction exits 2 on
    the 1.21.0 plugin cache and 0 on 1.21.5 (verified 2026-10-10).