Community寫作與編輯github.com

agent-sync — 一個專案,多個代理,無衝突

多個編碼代理、同一個儲存庫、無衝突——基於 TTL 的租約、無競爭的 ID 保留、運行日誌,以及在可插拔知識雲端上生成的看板。隨 sshlg-skills 套件一同發布。

agent-sync — 一個專案,多個代理,無衝突 是什麼?

`agent-sync` 是一個專為 AI 編碼代理(如 Claude Code)設計的協作框架,解決多個代理在同一 Git 儲存庫中並行工作時的核心問題:衝突與協調。它引入「雙平面」架構:Git 作為永久事實的記錄平面,雲端(或本地檔案)作為協調平面,存放正在進行中的狀態。關鍵功能包括帶 TTL 的租約(`acquire`/`release`/`renew`)、基於比較並交換(compare-and-swap)的 ID 保留、文件級別的守衛機制(`guard`)、跨儲存庫信號(`signal`),以及一個重新生成的共享看板(`board`)。它提供了從初始化到合併的完整生命週期:`init`、`scaffold`、`adopt`、`reconcile`、`merge`、`check`。租約可在本地(原子檔案建立)或透過 Git(非快進拒絕實現跨機器互斥)實現。`status` 命令是日常使用的重點,它會檢查健康狀態、回報其他活躍代理及其所持租約、顯示新的跨儲存庫依賴變更,並建議下一步動作。設計明確避免透過雲端儲存進行租約決策(因其缺乏原子性),並強調運行日誌的不可變性(只追加,不編輯)。此技能特別重視引導與驗證:`check` 會確認設定是否完整且一致,包括憑證、守衛規則、快照與基準線。它通過數百個 CLI 命令和 Claude Code 中的自動鉤子(`PreToolUse`、`PostToolUse`)來實現。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add ssheleg/agent-sync

Installed? Explore more 寫作與編輯 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

agent-sync — 一個專案,多個代理,無衝突 是做什麼的?

兩個平面,以及它們之間的一條規則:

Git 是記錄平面。雲端是協調平面。 必須留存的事實先寫入 Git,再從雲端引用。 關於現在誰在做什麼的事實存在於雲端,並且會過期。

沒有任何雲端物件是持久事實的唯一家。以下所有內容的存在,都是為了在多個代理同時寫入時維持這一原則。

四個陷阱 — 先讀這些,再做其他事

1. 知識庫永遠不能決定租約。 它做不到:對一個 Outline 文檔進行十二次並發附加操作,結果十二次都成功,但只留下了三行。排他性來自具有真正比較並交換能力的東西。平面只承載記錄,不做其他事。完整測量數據在 references/lease-protocol.md 中。

2. 知道你擁有哪種租約,並且說清楚。 leaseBackend: "local" 是原子檔案建立操作——在同一檔案系統的行程之間是獨占的,在不同機器之間是建議性的。leaseBackend: "git" 會推送一個引用,遠端因非快進而拒絕的機制就是比較並交換——在不同機器之間是獨占的。acquire 會印出是哪一種。假裝有租約比沒有租約更糟:另一個代理會停止檢查。

acquire 也會將聲明寫入路線圖,而 release 則會精確還原之前的內容——一行、一個儲存格,若有歧義則拒絕,在往返之後 git diff 為空白。在配置 claimTags 或關閉任務之前,請先閱讀 references/roadmap.md;關閉任務是關於工作完成狀態的陳述,並保持歸你所有。

在分支上工作;整合分支是其他人的穩定基礎。 acquire 僅在整合分支上寫入聲明;在任何其他分支上,持有者仍留在協調平面中,status 會向每個代理顯示該狀態,而無需任何人獲取你的分支。一旦提交到某個分支,聲明在合併之前是不可見的,並且會將共享的路線圖變成一個會被兩個分支編輯的檔案。使用 merge 來完成工作:衝突由 git merge-tree 在接觸任何東西之前進行計算、命名和拒絕(如果有衝突),合併記錄寫入 docs/MERGES.md(最近的幾天保持完整,較舊的會在寫入時壓縮),--key 租約被釋放。merges 會告訴下一個代理在你離開期間有哪些工作已合併。在合併前,請先閱讀 references/branching.md。

3. 鉤子僅存在於 Claude Code 中。 在其他地方,沒有任何東西能阻止受守衛的編輯:你自己執行 guard 並將此運行記錄為 ungated。不要將一個專案描述為受保護的,而實際上它並非如此。

4. 寬鬆地解析,永遠不要將不可讀的日誌稱為遺失的競賽。 儲存區會改寫你寫的內容——Outline 將 - 項目符號變成 * 。發出 - ,接受 -/*/+,對任何看起來像條目但解析失敗的內容進行計數,並且如果超過 2% 無法解析,則大聲失敗。當真相是不可讀時報告為 lost 會指認一個不存在的持有者。注意無聲的前置過濾器:在正則表達式之前的 continue 會將錯誤行隱藏起來,不讓用來揭露它們的計數器發現。

將此技能引入任何專案 — 完整的流程

scaffold  → 建立文件架構,僅在缺失的地方建立
adopt     → 讀取儲存庫,提出設定,不寫入任何內容
init      → 寫入已批准的設定 + 被 gitignored 的環境變數檔
            (操作者貼入 Token — 絕不是由你來做)
reconcile --set-baseline    → 使歷史成為一個可計數的待辦事項,僅一次
setup     → 生成描述此專案接線方式的快照
check     → 驗證整個設定;如果不健康,返回非零值

check 是讓此技能自給自足的關鍵。 如果規則保護的是不存在的東西(註冊表、守衛模式、聲明模式、閘道或鏡像來源指向不存在的路徑),如果缺少憑證,如果環境變數檔被 Git 追蹤(這是這裡唯一無法挽回的錯誤),如果快照過期或未連結,或者註冊表沒有基準線,它都會拒絕將設定標記為健康。它會一一指出每個問題;在此工具自身的採用過程中,每一個都曾真正失敗過。

在採用 (adopt) 之後、更改設定之後,以及在 CI 中,都應執行 check。

scaffold 永遠不會覆蓋現有內容。 它會在決策註冊表中植入一個分配行和一個指向快照的 AGENTS.md 檔案,並保持所有現有檔案不變——一個會在採用時改寫專案自身慣例的工具,比什麼都不做的工具更糟。

現有專案:從 adopt 開始

在 init 之前執行 adopt。它會讀取儲存庫並印出它所發現的內容——ID 註冊表、註冊檔、閘道——以及它拒絕為你做出的決定——然後提出一個設定。

python3 "$SKILL_DIR/scripts/agent_sync.py" adopt

首先與操作者確認註冊表和受守衛的檔案:指向錯誤檔案的註冊表會使之後的每一次檢查都自信地出錯,而遺漏共享檔案的守衛清單則會使可能發生衝突的唯一位置不受保護。在子模組中,它不會宣告任何註冊表:決策屬於父儲存庫。然後從上面的 init 開始執行後續流程。

專案中的第一個命令:init

永遠不要在未初始化的專案上執行任何其他命令。 init 是提出並回答儲存空間問題、且僅一次、並將其記錄下來的地方。

在聊天中向操作者詢問以下兩件事——不要猜測,不要選擇預設值:

  1. 協調狀態應該存在哪裡?

    • 知識雲端 (outline)——跨機器的共享記錄、感知和看板。它不決定租約;其中的任何東西都無法做到(陷阱 1);
    • 或本地檔案 (fs)——無需憑證,且對另一台機器上的代理不可見:無共享感知,無跨儲存庫訊號,無共享看板。

    租約由 leaseBackend 單獨決定(陷阱 2),而 gated 遵循該選擇,絕不遵循記錄平面。報告你實際擁有的保證,而不是記錄平面所暗示的更強的保證。

  2. 如果是雲端:提供實例 URL。 URL 是設定,不是機密,所以你可以寫下它。Token 則不是——你永遠不會在聊天中詢問它,永遠不會讀回它,也永遠不會自己放入它。

然後使用他們的答案執行它:

python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend outline --url https://<their-instance>
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend fs

init 會寫入 .claude/agent-sync.json(形狀,已提交),寫入 .env.agent-sync 並包含金鑰和一行空白的 Token(身份,權限 600),將 .env.agent-sync 和 .agent-sync/ 加入 .gitignore,然後精確印出操作者必須自己做的事——在他們自己的實例中建立 Token 並將其貼入那一行。沒有 --force 則永遠不會覆蓋現有設定或環境變數檔。

將這些結尾指示逐字轉達給操作者。將 Token 放入檔案是他們的步驟,而設計依賴於此 Token 一直屬於他們。

然後,在每次會話之前

python3 "$SKILL_DIR/scripts/agent_sync.py" status

冪等。檢查、修復、報告、命名恰好一個下一步行動——並且包含 check 的判定結果,因此每次會話都執行的命令與驗證設定的命令不會對同一個專案給出兩個不同的答案。

閱讀它印出的兩個感知區塊——它們是重點,不是裝飾。

  • 目前正在此專案上工作的其他運行。 誰持有什麼,當下這一刻。不要接手那些工作,也不要「只是看看」它們涵蓋的檔案。你看不到的租約使你被阻塞;你能看到的租約使你處於協調狀態。
  • 自你上次查看以來的新內容。 在你離開期間已完成的跨儲存庫依賴變更。已變更的依賴可能解除你計劃的阻塞——也可能使其無效。此清單每執行一次都會加上浮水印,因此它會在發生實際變化時才發聲;當它說話時,事情很重要。

跳過此區塊的代理將重新推導出其他人正在做的工作,並根據一小時前就已改變的依賴狀態採取行動。

status 還會決定什麼:沒有憑證 → 降級模式,會報告並繼續(一種較小的模式,不是錯誤);task-pipeline 缺失 → 印出安裝指令並停止。不要臨時湊合一個替代流程——沒有這些階段就沒有什麼可綁定的。

npx sshlg-skills install

命令列表

命令作用
init首先執行。 詢問狀態存在哪裡,寫入設定 + 被 gitignored 的環境變數檔,印出操作者的步驟
status檢查、修復、報告、命名一個下一步行動
bootstrap建立雲端容器並印出要貼入環境變數檔的 ID
acquire <KEY>取得任務 ID 的租約。印出 won 或 lost <holder>
renew <KEY>延長租約。PostToolUse 鉤子會為你做這件事
release <KEY>歸還租約。一定要做,包括在失敗時
reserve <REG>預留註冊表中的下一個 ID(DEC、OQ、DEP……)。印出 ID
release-id <REG> <ID>歸還你最終沒有寫入 Git 的 ID
journal <text>為此運行追加一行日誌
record <text>記錄你實際構建的內容——--decision DEC-…、--files a,b
reconcile意圖(Git)與實際構建(雲端)進行比較。每個專案一次 --set-baseline
signal <DEP-ID> <state>移動跨儲存庫依賴的狀態:filed/accepted/delivered/closed/refused
guard <path>回答此運行是否可以寫入該路徑。退出碼 0 = 可以,2 = 不可以
board重新生成共享看板和此儲存庫的頁面。--mirror 也會將設定的 Git 文檔渲染到平面中
whoami印出此運行的 ID 及其持有的租約
setup寫入此專案接線方式的生成快照,供代理讀取
adopt檢查現有專案並提出設定——不寫入任何內容
merge合併此分支:本地目標快進,衝突在接觸任何東西之前檢查完成,寫入合併日誌,釋放 --key 租約。--summary、--dry-run、--push
merges在你處理分支期間有哪些內容已合併。--all 包含已壓縮的尾部
check從頭到尾驗證整個設定。不健康時返回非零值
scaffold [--full]僅建立缺失的內容,絕不在任何現有內容上寫入一行。--full 還會植入問題註冊表、索引、依賴看板、帶有實體註冊表的資料模型,以及文檔閘道
finish [--gates]工作是否完成——每個儲存庫都乾淨、已推送並指向,沒有保留任何租約。check 回答專案是否接線正確;此命令回答你是否完成

$SKILL_DIR 是此技能自己的目錄:在 Claude Code 外掛下為 ${CLAUDE_PLUGIN_ROOT}/skills/agent-sync,在其他地方為 ~/.agents/skills/agent-sync。每次會話解析一次並重複使用——不要猜測路徑。每個命令都從專案根目錄讀取 .claude/agent-sync.json,並且除了列出的參數外不需要其他參數。

每個會話一個身份,以及它是如何決定的

只有在兩個代理獲得兩個不同的身份時,租約才有效。順序在這裡很重要,而且兩端都曾出過問題:僅從 CLAUDE_SESSION_ID 推導 ID 導致一個會話有兩個身份——它以一個身份獲取租約,卻被自己的守衛以另一個身份拒絕——而為每個檢出目錄保留一個 ID 則導致兩個會話共享一個身份,這更糟。後者是無聲的:兩個會話都獲取租約,兩個都被守衛,而 release 會拿走一個呼叫者從未擁有的租約。

順序是:AGENT_SYNC_RUN_ID · CLAUDE_SESSION_ID · 啟動此 shell 的會話 · 共享。

第三項之所以重要,是因為普通的 shell 命令沒有會話 ID,而鉤子有。因此 SessionStart 會將 .agent-sync/sessions/<CLI pid> 標記為它所知道的會話,而後續的命令通過遍歷自己的行程祖先鏈到一個被標記的 PID 來找到自己。為什麼是這個而不是命令列解析:見 references/earned-rules.md。

當這四個都無法確定時,運行會說出來——「此身份與此檢出目錄中的任何其他會話共享」——而不是將一個共享的條目呈現為分離的。

聲明 — 重要的形狀

acquire → 執行工作 → release

永遠不要跳過 release,包括在失敗時:被放棄的租約會阻塞任務直到其 TTL 過期,而下一個代理無法區分「進行中」和「一小時前崩潰了」。

租約不是聲明。 租約說明誰現在持有它並且會過期;持久的聲明是 Git 中的標籤,由 acquire 寫入並由 release 清除。一個事實,一個家——不要發明記錄所有權的第三個地方。

在更改獲取、過期、搶佔或 ID 分配邏輯之前,請閱讀 references/lease-protocol.md。

受守衛的檔案

設定檔列出了多個代理可能會寫入的註冊表檔案。在編輯其中一個之前:

python3 "$SKILL_DIR/scripts/agent_sync.py" guard docs/DECISIONS.md

退出碼 2 是關於此次運行:它沒有持有租約——而不是別人持有該檔案。一個租約涵蓋所有受守衛的檔案;要嘛持有一個,要嘛什麼都不寫。拒絕會指名另一個運行及其金鑰,因為「r-x 持有租約」緊鄰著路徑會被重複為「r-x 持有此檔案」。不要無論如何都要編輯,也不要「只修復一行」——一個被覆寫的決策看起來和一個決策一模一樣。

Claude Code 的 PreToolUse 鉤子會為你執行此操作。其他地方則不會。

預留 ID

讀取「下一個可用 ID」行並不是在預留它——兩個代理讀取到同一個號碼並同時使用它。

python3 "$SKILL_DIR/scripts/agent_sync.py" reserve DEC   # → DEC-0216

分配是基於合併後的日誌進行位置性的——包含所有分片,絕不僅是此次運行的——因此每個代理都計算出相同的答案。預留了但沒有寫入 Git?對它執行 release-id,否則這個號碼就是一個漏洞,看板會將其報告為洩漏。

日誌中的任何內容都不得編輯或刪除

日誌會按順序重放,因此編輯會無聲地改寫其他代理已經據以行動的結論。通過追加來糾正:釋放租約,對未使用的 ID 執行 release-id,用後續條目取代錯誤的實際構建條目。生成的頁面是唯一的例外,但任何遺失其 agent-sync:generated 標記的頁面……

…[因翻譯截斷]

相關技能