簡而言之

  • 我同時執行的 Claude Code 工作階段中位數是八個,同一個 checkout 最多到十個。三十天內,有 118 個列出了其他工作階段,7 個送出了訊息。
  • 一個工作階段需要知道的同儕資訊,全都已經在磁碟上:即時工作階段登錄表、每一則輸入的提示、每個工作階段寫過的每個檔案。
  • 三個 hook 去讀它們。工作階段啟動時顯示誰在這裡、上一個工作階段做了什麼;每則提示顯示同儕寫進記憶的內容;當有同儕共用同一個 checkout 時,拒絕全索引的 git 操作。
  • 「編輯前要先宣告」這條規則載入十分鐘後,就有一個工作階段主動告訴另一個它即將要動的檔案。我會在十月底重新量測訊息數。

我執行很多 Claude Code 工作階段。到 9 月 21 日為止的三十天逐字記錄:297 個工作階段,一天中任一時點存活的中位數是八個,尖峰十二個,同一個 checkout 裡最多十個。其中一半存活超過五小時。十分之一存活超過 39 小時。

它們具備協調所需的一切能力。ListAgents 會列出機器上其他存活的工作階段;SendMessage 可以用名字聯絡其中任何一個。在那三十天裡,118 個工作階段呼叫了 ListAgents,7 個送出了訊息。它們缺的是說話的理由,以及可以據以行動的事實,而這兩者早就在磁碟上了。

問題:一個 checkout、八個工作階段、七則訊息

一個 checkout 上有八個工作階段,旁邊掛著三條帶狀資料:git 索引與工作樹,每個工作階段都在即時讀寫;記憶目錄,每個工作階段在啟動時讀一次;還有登錄表、逐字記錄與歷史,每個工作階段每一輪都在寫,卻沒有任何東西去讀 前兩條帶狀資料是工作階段共用的。第三條是它們全都在寫,而在此之前沒有任何東西去讀的。

同一個 checkout 裡的工作階段共用兩樣東西。git 索引與工作樹:整棵樹的操作(git add -A、commit -a、沒有指定路徑的 stash)會作用在所有人的變更上,而 git 完全不知道哪個檔案屬於哪個工作階段。還有以儲存庫為鍵的自動記憶目錄,它在工作階段啟動時載入一次:一個從 08:00 就開著的工作階段,永遠看不到同儕在中午寫下的更正。其他一切都是私有的。從外面看,一個同儕就是 life-os-a9 這樣的名字加上一個狀態詞。

接著出現三種失敗,每一種都量測過:

  • 被吸收的工作。 9 月 1 日,一個工作階段暫存了六個檔案;幾秒後第二個執行了範圍很廣的 git add,把它們掃進自己的提交裡。第一個工作階段唯一的症狀,是 git commit 印出「no changes added to commit」。
  • 重新學過的規則。 mistboard 的記憶裡重新學了三條已經存在 life-os 記憶中的規則,只是用了不同的字句。
  • 盲目的啟動。 那個月有 302 個工作階段結束;2 個寫了交接。每個新工作階段都是從零開始。

想法:去讀這個程序本來就在寫的東西

Claude Code 維護著三個沒人在讀的檔案。~/.claude/sessions/<pid>.json 存放每個存活工作階段的 id、工作目錄、名稱、狀態與啟動時間。~/.claude/history.jsonl 存放每一則輸入的提示以及它的工作階段 id。每個工作階段的逐字記錄存放每一次工具呼叫,包括它寫過的每個檔案路徑。同儕會想知道的一切都在裡面,而且由程序自己維護。

所以每一個元件的規則是:讀磁碟上已有的東西,絕不新增任何需要工作階段自行維持更新的欄位。手寫交接的撰寫率只有 1%;一個狀態欄位不會更好。逐字記錄的第一則提示本身就是一份目的陳述,工作階段不可能忘記寫它。

實作過程中又衍生出三條規則:

  • 把每個元件掛在它的事實最新鮮的時刻:工作階段啟動、每則提示,或指令執行前的那一刻。
  • 拒絕,不要上鎖。一個會指名同儕並提出替代方案的守衛,勝過一個持有沒人看得見的鎖的守衛。
  • 把指示放在做決定的地方。放在工作階段從不載入的檔案裡的規則,預設就會被違反。

解法:三個 hook 上的四個元件

一個工作階段的時間軸:工作階段啟動時,同儕列表讀取登錄表、歷史與逐字記錄,同時載入規則;每則提示時,記憶差異讀取記憶檔案的時間與本工作階段的逐字記錄;每個 shell 指令前,git 守衛讀取登錄表與 git 頂層目錄 每個元件都掛在它的事實最新鮮的那個 hook 上,並讀取第一張圖裡的那些檔案。

所有接線就是 ~/.claude/settings.json 裡的一個區塊。hook 的 stdout 會進入工作階段的脈絡;結束碼 2 會拒絕該工具呼叫,並以 stderr 作為理由。這些腳本、它們的測試與量測都在 claude-code-peers;下面節錄的是重點部分。

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|resume",
      "hooks": [{ "type": "command", "timeout": 10,
                  "command": "python3 scripts/peers.py --hook" }]
    }],
    "UserPromptSubmit": [{
      "hooks": [{ "type": "command", "timeout": 5,
                  "command": "python3 scripts/memory_delta.py" }]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "timeout": 5,
                  "command": "python3 scripts/git_index_guard.py" }]
    }]
  }
}

工作階段啟動:上一個在這裡的是誰,現在誰在這裡。 peers.py --hook 會印出在這個 checkout 裡最近結束的工作階段(何時結束、執行多久、在做什麼、編輯了什麼),接著是存活的同儕。第一個區塊就是沒人會寫的交接;如果手寫的 HANDOFF.md 存在就會指向它,而當它比它應該描述的工作階段還舊時就會被標記出來。意圖來自提示歷史,並去掉那些確認語句:一到四個字的提示佔了那個月的 35%,所以下限設為五個字。同儕會顯示第一則有實質內容的提示、倒數第二則與最後一則,因為意圖會漂移,而最後兩則能顯示漂移的方向。它正在編輯的檔案來自逐字記錄,從後往前讀取最近的 Write 與 Edit 呼叫。

- mistboard-08  busy · up 36h · 161 prompts · last 1m ago
    opened:  "can you investigate this game: https://mistboard…"
    before:  "this widget is still the old style, we gotta use…"
    now:     "let's also frame it as they won it, since the…"
    editing: apps/web/src/jungle-replay-board.ts (1m ago)

每則提示:從你啟動以來,同儕寫進記憶的內容。 memory_delta.py 會 stat 記憶目錄,並列出在本工作階段啟動之後被修改的檔案,每個只列一次,附上寫入者的名字。自己的寫入會透過檢查逐字記錄中對該路徑的寫入而排除。大約 40 毫秒,而且在大多數提示上都是靜默的。

threshold = max(session_start, last_run)  # last_run: a stamp file
for f in memory_dir.glob("*.md"):
    if f.name == "MEMORY.md" or f.stat().st_mtime <= threshold:
        continue
    desc, origin = frontmatter(f)  # description:, originSessionId:
    if origin == session_id or written_here(f, transcript):
        continue                           # this session's own write
    print(f"- {f.name} ({when(f)}, {peer_name(origin)}): {desc}")

每個 shell 指令:有同儕共用這個 checkout 時,不允許全索引的 git 操作。 git_index_guard.py 會解析指令中的 git add -A、add .、commit -a 以及沒有路徑的 stash,解析出該指令會動到的 checkout(包含 cd X && 與 git -C X),並讀取登錄表找出同一個 git 頂層目錄上其他存活的工作階段。worktree 有自己的索引,一律不計入。若在一個 checkout 裡獨自作業,指令會原封不動地執行。

def peers_in(top, own_session_id):
    """Other live sessions whose checkout is this git toplevel."""
    return [j["name"] for j in live_sessions()
            if j["sessionId"] != own_session_id
            and toplevel(j["cwd"]) == top]   # a worktree has its own

peers = peers_in(toplevel(git_cwd), payload["session_id"])
if peers:
    sys.stderr.write(
        f"Refused: `{what}` while {len(peers)} other live session(s) "
        f"share this checkout ({', '.join(peers)}). The git index is "
        "shared; name the paths this session changed instead.\n")
    sys.exit(2)

每個工作階段、每個儲存庫:那些一直被重新學過的規則。 ~/.claude/rules/ 裡的六個 markdown 檔案,以符號連結指向一個有版本控管的目錄,會載入機器上的每一個工作階段:共用樹的行為準則、token 預算、哪些帳號可以在哪裡發佈、在聲稱沒人做過某件事之前要先檢查什麼。每一條都指名存放其證據的記憶檔案。每次工作階段啟動約兩千個 token,有快取。

每個元件都有一個 fixture 測試,用 Claude Code 的方式透過 stdin 驅動它,另外還有一個接線檢查,確認每個預期的 hook 都已註冊、存在、能編譯,並通過它的測試。

目前的結果

接線是在傍晚完成的。隔天早上,life-os 裡在兩個同儕在場的情況下執行 git add -A 被拒絕,並指名了兩者。一個全新工作階段對「現在什麼情況」的第一個回答,開頭就是三個同儕以及各自在做什麼,接著是上一個結束的工作階段,而我什麼都沒貼進去。而記憶 hook 在一個存活工作階段裡的第一行是錯的:一個同儕數週前建立、我剛剛編輯過的檔案,被顯示成同儕的,因為 originSessionId 指的是建立者,而不是最後的編輯者。現在,對於工作階段並非建立者的每個檔案,都會執行逐字記錄檢查。

真正重要的那則訊息,出現在一段文字放進每個工作階段都會載入的 CLAUDE.md 之後十分鐘:在編輯同一個 checkout 的同儕提示中提到的檔案之前,或在同儕忙碌時提交之前,先告訴它。那段文字在 00:08 落地。這個部落格儲存庫裡的一個工作階段在 00:18 啟動。大約 00:25,它主動把這則訊息送給 life-os 的工作階段(檔案清單已裁剪):

來自 brianhliou.com 工作階段的提醒:我正在 life-os 裡做更名清理:scripts/lesson_harvester.py、data/lessons.jsonl(去重 173 筆重複記錄)、CLAUDE.md:151、兩份摘要、透過 CLI 的兩則筆記,然後執行 lesson_compile.py –apply。會按名稱暫存;不會動到你的檔案。

這則宣告抵達 life-os 工作階段,以及它送出的一行回覆 接收端工作階段的視角:抵達時的訊息,以及它送出的回覆。

回覆只有一行:沒有重疊,請繼續,加上一項它本來不可能知道的事實,就是 lesson_compile.py 一小時前在 main 上有過變更。那項工作最後落在一個提交裡,動到七個檔案,沒有一個屬於接收端。總共三則訊息。這次交流裡沒有任何新能力;在它發生前那十分鐘改變的是:規則載入到做決定的地方、工作階段能看見同儕正在編輯什麼,而且範圍很廣的 add 本來就會被拒絕。

依照改變程度排序:交接第一,因為每個工作階段以前都是盲目啟動;守衛第二,因為它是唯一能防止損害而不只是描述損害的元件;訊息最後,因為它是你唯一看得見的。

下一步:一個月後的數字

限制是已知的。守衛只會拒絕;它不上鎖,所以同儕的 git reset --hard 仍然會回復你未提交的編輯,而關於這件事的規則只是文字。記憶 hook 只涵蓋記憶目錄,不包含 CLAUDE.md,所以工作階段中途被編輯的規則,只有在壓縮時才會傳到正在執行的工作階段。登錄表與逐字記錄都是本機的,所以雲端工作階段對這一切都是看不見的。

這些原本都不該由我來做。登錄表、逐字記錄、歷史檔案、hook 與訊息機制,全都隨 Claude Code 一起提供。沒有隨附的,是把它們連起來的預設值,所以同一台機器上的八個工作階段就像八個陌生人,直到有人去量測並接線。我預期這個預設值會出現在產品裡;aide 一開始是我自己在同一批逐字記錄上架的 SQLite,後來產品也在同一批日誌上長出了 /insights。在那之前,接線就是一個設定區塊加三個腳本,而你手上已有的逐字記錄就是起點。

要檢驗的主張是:顯示意圖會讓工作階段開始對話。基準線是三十天內有七個工作階段送出訊息。我會在十月底重新量測,並帶著數字回來更新這篇文章。如果數字沒有動,那麼名單從來就不是限制所在,接下來要看的是究竟什麼才是。