以 worktree 隔離平行工作包的實作與修正 #38

Closed
opened 2026-09-17 07:11:13 +00:00 by jiantw83 · 0 comments
Member

Problem Statement

同一個開發者在同一份 clone 上同時持有多顆已領取的工作包時,目前的流程要他在一個工作目錄上反覆切換分支。這件事帶來三種損耗,一種比一種難查:

  1. 未提交的變更擋路:git checkout 要嘛拒絕切換,要嘛把變更帶到另一個分支上,兩種都要人當場處理。
  2. 建置產物與依賴跨分支混淆:A 分支裝的套件、產生的建置快取,被 B 分支讀到,跑出來的結果不是任何一個分支真實的樣子。
  3. agent 讀到不屬於它那顆工作包的程式碼:這是本工具特有的風險。agent 是非同步的——它可能在分支已經被切走之後才去讀檔,而它不會察覺自己讀到的是別顆工作包的內容。產出看起來完全合理,只是接錯了上下文。這種錯不會在當下爆炸,會在 review 甚至上線後才浮現。

第 3 點是「一律使用工作樹」而非「有衝突才用」的理由:前兩種損耗人會當場發現,第三種不會。

Solution

/sdlc-feat 領取工作包時不再原地切換分支,而是一律在一棵獨立的**工作樹(worktree)**上開工。每顆工作包有自己的目錄、自己的建置產物、自己的未提交變更,彼此看不見對方,agent 無論什麼時候去讀檔都只會讀到它該讀的東西。

工作樹集中放在 ~/.tea-sdlc/worktrees/ 底下,目錄名由 owner/repo/分支名 推導而得,因此任何流程都能從「這是哪顆工作包」純函式地算出「它的工作樹在哪」,不需要任何本機對照表或狀態檔。換機器、換 agent 時推導結果一樣,工作樹不存在就重建。

PR 開立之後,一支無狀態的檢查腳本回報 PR 現況與待辦(還有幾則留言沒處理、工作樹在哪、有沒有未提交的東西),由使用者決定要不要動手。PR 合併或關閉時,它自動清掉那棵工作樹——這是它唯一會自動執行的副作用,而且完全可逆。

取代宣告

本需求取代既有工作包的部分內容。#11 與 #14 本身不修改,實作時以本議題為準。

取代 #11 的驗收標準第 4 條:

詢問來源分支;來源分支在遠端已存在時執行 pull 而非重建

改為:

一律以 git fetch 更新遠端引用後,從 origin/{來源分支} 長出新分支與工作樹;遠端不存在該來源分支時明確中止。

原條文的用意是「不覆蓋他人進度」,新寫法用更強的方式達成同一件事——根本不碰本機分支,就沒有覆蓋的可能。同時 #11 第 5 條的分支命名規則不變,只是分支建立的動作與工作樹建立合併為同一個原子操作。

補充 #14 缺少的部分:#14 沒有工作樹的概念,預設在當前目錄處理留言。本需求為它補上「由工作包推導工作樹路徑,不存在就重建」。#14 既有的驗收標準全部仍然成立,這是疊加而非改寫。

User Stories

  1. 身為同時持有多顆工作包的開發者,我想讓每顆工作包有自己的工作目錄,以便切換工作包時不必先處理未提交的變更。
  2. 身為開發者,我想讓每顆工作包有自己的建置產物與依賴,以便 A 分支裝的套件不會被 B 分支讀到。
  3. 身為開發者,我想讓 agent 無論什麼時候讀檔都只讀到它那顆工作包的程式碼,以便不會產出接錯上下文、事後才發現的東西。
  4. 身為開發者,我想讓工作樹的建立是一律的而非條件式的,以便不必自己判斷「這次會不會衝突」。
  5. 身為開發者,我想在工作樹建立失敗時被明確擋下,以便不會以為自己在隔離環境裡、其實在原地改。
  6. 身為開發者,我想讓工作樹集中在一個固定的家目錄底下,以便清理時只有一個地方要看。
  7. 身為開發者,我不想讓工作樹出現在目標專案裡,以便 git status 不會多出東西、也不必改目標專案的忽略設定。
  8. 身為開發者,我不想讓工作樹長在目標專案的父目錄,以便我自己的專案目錄結構不被這個工具塑形。
  9. 身為開發者,我想讓工作樹路徑由工作包純函式推導,以便換機器或換 agent 之後仍然找得到同一棵。
  10. 身為開發者,我想在推導出的工作樹不存在時自動重建,以便在新機器上接手不必手動補。
  11. 身為開發者,我想讓新分支一律從遠端的來源分支長出,以便起點永遠是最新的、不會是落後好幾天的本機分支。
  12. 身為開發者,我想在遠端沒有指定的來源分支時被明確擋下,以便不會靜默地退回一個錯的起點。
  13. 身為開發者,我想讓建分支與建工作樹是同一個動作,以便失敗時不會留下有分支沒工作樹、或有工作樹沒分支的半成品。
  14. 身為開發者,我想讓碼錶在工作樹建成之後才起動,以便一次失敗的領取不會在工時報表上留下一段憑空的時間。
  15. 身為開發者,我想在工作樹建好時被告知它是乾淨的,以便知道要先裝依賴才能跑起來。
  16. 身為開發者,我不想讓任何機密檔案被自動複製到工作樹,以便不會在意料之外的地方多出一份憑證。
  17. 身為開發者,我想知道 PR 現在的狀態與還有幾則留言沒處理,以便決定要不要現在動手。
  18. 身為開發者,我想讓這個查詢是一次性的、無狀態的,以便可以用我自己的排程去跑,而不是被迫開著一個終端機。
  19. 身為開發者,我想讓查詢結果包含工作樹的路徑與是否有未提交變更,以便一眼看出能不能直接接著做。
  20. 身為開發者,我想讓「建議動作」是固定的列舉值,以便我的排程能程式化判斷而不是去解讀一段文字。
  21. 身為開發者,我不想讓任何流程在我沒下指令時自動改我的程式碼,以便我永遠知道是誰動的手。
  22. 身為開發者,我想在 PR 合併或關閉時讓工作樹被自動清掉,以便集中目錄不會無限長大。
  23. 身為開發者,我想在 PR 被退回草稿時保留工作樹,以便繼續改的時候東西還在。
  24. 身為開發者,我想在工作樹裡還有未提交變更時讓清理被擋下,以便不會被自動流程刪掉還沒存的東西。
  25. 身為開發者,我想在清理工作樹時保留本機與遠端分支,以便之後還能回頭看那段歷史。
  26. 身為開發者,我想有一個手動清理的出口,以便處理那些永遠不會被合併也不會被關閉的 PR。
  27. 身為開發者,我想讓 sdlc-fix 自己定位到正確的工作樹,以便處理留言時不必先手動 cd。
  28. 身為開發者,我想讓 sdlc-fix 在我從來沒跑過監看腳本的情況下也能自己發現 PR 已經合併,以便不會在一棵該被清掉的工作樹上白做工。
  29. 身為開發者,我想讓工作樹的隔離只管檔案、不管工時,以便工時報表的數字仍然是「這段時間你花在這件事上」。
  30. 身為開發者,我想在因為碼錶還在跑而被擋下領取時,被明確告知停錶不會動到既有的工作樹,以便不會以為停錶等於放棄那顆工作包。

Implementation Decisions

工作樹路徑

集中在 ~/.tea-sdlc/worktrees/{hash},其中 hash 為 sha256 對正規化後的 owner/repo/分支名(小寫、去前後空白)取值後的前 12 碼。

選集中式而非 repo 內的 .worktrees/:後者未被忽略時會出現在目標專案的 git status,而要它不出現就得改目標專案的忽略設定,那是 #1 的 Out of Scope 明文禁止的。選集中式而非 repo 的兄弟目錄:兄弟目錄會在使用者的專案父目錄長出一堆東西,而那個目錄結構屬於使用者,不屬於這個工具。

選雜湊而非把分支名的斜線攤平成 -:攤平會讓 feat/a-b/main 與 feat/a/b/main 撞成同一個目錄名,而既有的分支命名規則({類型}/{需求描述}/{功能描述})恰好讓這種形狀有機會出現。雜湊沒有可讀後綴,可讀性的缺口由兩處補上:git worktree list 的輸出本來就會把分支名印在路徑旁邊,而 PR 檢查腳本會回報推導出的路徑。

正規化是必要的——沒有它,同一棵工作樹會因為輸入大小寫或多一個空白而被推導成兩個不同路徑。

必須明說的邊界:git worktree add 一定會在目標 repo 的 .git/worktrees/ 底下寫中繼資料,這是 git 的機制,無法避免。#1 的 Out of Scope 所說「不修改目標專案的檔案」指的是專案內容檔(CLAUDE.md、設定檔之類),不含 git 自己的內部中繼資料。

建立流程

git worktree add -b {新分支} {推導路徑} origin/{來源分支} 是一個原子動作,同時建立分支與工作樹。之前先 git fetch 更新遠端引用——git worktree add 從一個既有的 ref 長出,來源不新則起點就不對。

遠端不存在指定的來源分支時明確中止,錯誤訊息指出「請先把來源分支推上去,或改指定一個已存在的來源分支」。不退回本機同名分支:那是靜默降級,而且後果隱蔽——使用者以為自己從最新的遠端狀態開工,實際上起點可能落後好幾天。

不設 upstream。此刻遠端還沒有這個新分支,--track origin/{來源} 會把 upstream 指到來源分支,之後 git pull 會把來源分支的新提交拉進來,幾乎一定不是使用者要的。upstream 留給 #13 的第一次 push -u 自然建立。

起錶時機後移:碼錶在工作樹建成之後才起動。#11 現行的隱含順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立失敗會中止——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,而工時要準正是 /sdlc-report 的立足點。

工作樹裡的開發環境

不複製也不連結 node_modules、vendor、建置快取。symlink 特別危險:兩棵工作樹共用同一份 node_modules,A 分支的安裝會改到 B 分支讀到的東西,正好把工作樹要隔離的東西又接回去,而且是以最難察覺的方式。複製則昂貴且立刻過期。

改為建立後印出提示,安裝指令借用 #12 已經要做的語言偵測產生(偵測到 package.json 提示 npm install、composer.json 提示 composer install,偵測不到就只說「這是一棵乾淨的工作樹」)。.env 這類機密絕不自動複製,只提示使用者自己放。

PR 監看

一支一次性的檢查腳本,具名 flag 進、單行 JSON 出、無狀態、冪等。不做常駐程序也不做 daemon:daemon 需要 pidfile,而 #11 明定「不在本機留任何狀態檔,換機器或換 agent 都能接手」;常駐前景程序雖然不留檔,卻把「監看中」這個狀態綁在一個終端機 session 上,同樣違反那條規則的精神。排程交給呼叫端(cron、或 agent 工具自己的循環機制),本工具不長出排程器。

不做變化偵測。「已處理」的判定基準是 #14 定下的 +1 reaction,也就是這個狀態已經存在 Gitea 上、不在本機記憶裡,所以現況快照本身就足以回答「還有沒有事要做」。比較前後兩次結果是個不存在的需求。

回傳內容:PR 狀態(open/merged/closed/draft)、未處理留言數、工作樹現況(推導路徑、是否存在、有無未提交變更)、terminal、cleaned、以及列舉值形式的 suggestedAction(run-sdlc-fix/cleanup/nothing-to-do/blocked-dirty)。用列舉值而非自由文字,呼叫端才能程式化判斷。

三種 PR 狀態的處置不一致:merged 與 closed 是終止狀態,terminal: true 並自動清理工作樹;draft 繼續監看且不清理——被退回草稿代表還要繼續改,這時候使用者更需要那棵工作樹。

監看只通知,不動手

偵測到有未處理留言時只印出建議,不自動執行 /sdlc-fix。理由有兩條,都是硬的:#1 的使用者故事第 77 條明定「不想讓這些流程被模型自動觸發」;而 #14 的驗收標準要求「分類或做法不確定時詢問使用者,不自行決定」,在非互動模式下那個詢問無處可去,agent 只能自行決定,等於把一條驗收標準做成謊言。

唯一自動執行的副作用是清理工作樹,而那件事完全可逆(隨時能重建)。

/sdlc-fix 另外自行檢查 PR 狀態,補上「使用者從來沒跑過監看腳本」的缺口。

清理範圍

只 git worktree remove,本機分支與遠端分支都保留。遠端分支的刪除是 Gitea 合併時的選項,由使用者在網頁上決定,本工具代勞就越界(同 #1 Out of Scope 那條「不提供議題的刪除或關閉流程」的精神)。本機分支不佔什麼空間,留著讓使用者還能回頭看。

絕不加 --force:工作樹內有未提交變更時明確中止並報出路徑。這條之所以重要,是因為清理會被自動執行——而自動執行的東西只能做可逆的事。

腳本歸屬

branch-prep 吸收工作樹建立(Q14 已定它與建分支是同一個原子動作,拆開必然有一支做半套)。新增 pr-watch 與 worktree-remove 兩支,共用同一份移除實作——pr-watch 直接呼叫共用函式,不另起子行程。worktree-remove 存在的意義是給使用者一個手動出口,處理那些永遠不會被合併也不會被關閉的 PR。#1 的腳本清單因此再長出這兩支。(實作後另由 #42 追加第三支 worktree-ensure:/sdlc-fix 要能由工作包定位工作樹、不在就重建,而那條路徑需要自己的入口。)

工時層不變

工作樹解決的是檔案層的互相影響,不是工時層的。#1 的領取規則(自己碼錶跑在任何議題一律擋)維持不變:工時本來就該是序列的,一次只能把時間花在一件事上。

這條規則有個容易被誤認為冗餘的地方,要寫下來:Gitea 在不同議題上起新錶時,會回 201 成功並靜默地停掉且記錄前一顆錶(HasUserStopwatch 回傳單一碼錶,CreateIssueStopwatch 建立前先 Finish 既有的那顆;409 只發生在同一顆議題上重複起錶)。所以「自己碼錶跑在別的議題也擋」是本工具刻意加的保護——沒有它,claim 會靜默結算使用者上一段工時。

附帶要改的只有錯誤訊息:被碼錶擋下時要明說「停錶不會動到你既有的工作樹」,否則使用者會以為停錶等於放棄那顆工作包。

Testing Decisions

什麼是好的測試:只測外部行為。對本需求而言,外部行為是「給定一組 flag 與一份 Gitea 回應,腳本印出什麼 JSON、對 Gitea 發出哪些請求、以及對 git 做了什麼」。

接縫維持既有的那一個:scripts/*.js 的 CLI 邊界,以子行程執行、比對 stdout 的 JSON 與 exit code。不新增接縫。

git 操作不做 mock:沿用既有做法,在臨時 git repo 上跑真實 git。工作樹是 git 的功能,用假的 git 測工作樹等於什麼都沒測。臨時 repo 需要一個「遠端」才能測 origin/{來源分支},以本機裸 repo 充當即可,不需要網路。

測試對象優先序:

  1. 工作樹路徑推導 —— 純字串與雜湊運算,表格驅動,成本最低回歸價值最高。要涵蓋:大小寫與空白的正規化(同一輸入的變體必須推導出同一路徑)、分支名含斜線、feat/a-b/main 與 feat/a/b/main 不撞名。
  2. 建立流程的失敗路徑 —— 遠端不存在來源分支時中止且不留下任何分支或工作樹;工作樹已存在時的行為;起錶確實發生在工作樹建成之後(失敗案例中不得有起錶請求)。
  3. 清理的守門 —— 工作樹內有未提交變更時中止並報路徑,且工作樹仍然存在;乾淨時移除成功且本機分支仍在。
  4. PR 狀態到處置的對照表 —— open/merged/closed/draft 四種狀態各自的 terminal、cleaned、suggestedAction,特別是 draft 必須 terminal: false, cleaned: false。
  5. --dry-run —— 所有寫入型操作在 --dry-run 下輸出「將執行的 git 指令與將發出的請求」而不實際執行。

Prior art:既有的腳本契約測試已用子行程加 stub server 驗過腳本契約與四層前置檢查,臨時 git repo 的 helper 也已存在,兩者都可直接沿用。測試執行器維持 Node 內建的測試器,暫存一律寫到已被忽略的暫存目錄。

Out of Scope

  • 不處理多人協作:工作樹只隔離「同一個人、同一份 clone」的多顆工作包。兩個人各自 clone 本來就互不影響。
  • 不放寬碼錶規則,不支援同時計時多顆工作包。
  • 不自動執行 /sdlc-fix,即使監看腳本已經偵測到有未處理的留言。
  • 不複製、不連結 node_modules、vendor、建置快取或任何機密檔案到工作樹。
  • 不刪除本機分支,不刪除遠端分支。
  • 不以 --force 移除含未提交變更的工作樹。
  • 不提供常駐程序、daemon 或任何形式的排程器。
  • 不修改目標專案的忽略設定或任何專案內容檔。
  • 不修改 #1、#11、#14 三顆既有議題。

Further Notes

  • 目前 repo 內完全沒有任何工作樹相關實作或提及,這是全新的一條分支,不是修改既有行為。
  • 本機 git 版本 2.47.3,工作樹功能完整支援。
  • 相依關係:本需求的工作包 A 掛在 #11 之下,B+C 與 D 掛在 #14 之下並同時被 A 擋住。A 是 #11 的旁支,不在 #11→#12→#13→#14 那條主鏈上,所以 #14 完成不蘊含 A 完成——A → B+C 與 A → D 這兩條邊是真實的實作順序限制,不是為了好看。
  • 已知風險(已被接受):#11 為 open 且 ready-for-agent,任何人或 agent 領走它讀到的仍是被本議題取代的條文。緩解方式是本議題的「取代宣告」段,但它只能被讀到本議題的人看到。
## Problem Statement 同一個開發者在同一份 clone 上同時持有多顆已領取的工作包時,目前的流程要他在一個工作目錄上反覆切換分支。這件事帶來三種損耗,一種比一種難查: 1. **未提交的變更擋路**:`git checkout` 要嘛拒絕切換,要嘛把變更帶到另一個分支上,兩種都要人當場處理。 2. **建置產物與依賴跨分支混淆**:A 分支裝的套件、產生的建置快取,被 B 分支讀到,跑出來的結果不是任何一個分支真實的樣子。 3. **agent 讀到不屬於它那顆工作包的程式碼**:這是本工具特有的風險。agent 是非同步的——它可能在分支已經被切走之後才去讀檔,而它**不會察覺**自己讀到的是別顆工作包的內容。產出看起來完全合理,只是接錯了上下文。這種錯不會在當下爆炸,會在 review 甚至上線後才浮現。 第 3 點是「一律使用工作樹」而非「有衝突才用」的理由:前兩種損耗人會當場發現,第三種不會。 ## Solution `/sdlc-feat` 領取工作包時不再原地切換分支,而是一律在一棵獨立的**工作樹(worktree)**上開工。每顆工作包有自己的目錄、自己的建置產物、自己的未提交變更,彼此看不見對方,agent 無論什麼時候去讀檔都只會讀到它該讀的東西。 工作樹集中放在 `~/.tea-sdlc/worktrees/` 底下,目錄名由 `owner/repo/分支名` 推導而得,因此任何流程都能從「這是哪顆工作包」純函式地算出「它的工作樹在哪」,不需要任何本機對照表或狀態檔。換機器、換 agent 時推導結果一樣,工作樹不存在就重建。 PR 開立之後,一支無狀態的檢查腳本回報 PR 現況與待辦(還有幾則留言沒處理、工作樹在哪、有沒有未提交的東西),由使用者決定要不要動手。PR 合併或關閉時,它自動清掉那棵工作樹——這是它唯一會自動執行的副作用,而且完全可逆。 ## 取代宣告 本需求取代既有工作包的部分內容。**#11 與 #14 本身不修改**,實作時以本議題為準。 **取代 #11 的驗收標準第 4 條**: > ~~詢問來源分支;來源分支在遠端已存在時執行 pull 而非重建~~ 改為: > 一律以 `git fetch` 更新遠端引用後,從 `origin/{來源分支}` 長出新分支與工作樹;遠端不存在該來源分支時明確中止。 原條文的用意是「不覆蓋他人進度」,新寫法用更強的方式達成同一件事——根本不碰本機分支,就沒有覆蓋的可能。同時 #11 第 5 條的分支命名規則**不變**,只是分支建立的動作與工作樹建立合併為同一個原子操作。 **補充 #14 缺少的部分**:#14 沒有工作樹的概念,預設在當前目錄處理留言。本需求為它補上「由工作包推導工作樹路徑,不存在就重建」。#14 既有的驗收標準全部仍然成立,這是疊加而非改寫。 ## User Stories 1. 身為同時持有多顆工作包的開發者,我想讓每顆工作包有自己的工作目錄,以便切換工作包時不必先處理未提交的變更。 2. 身為開發者,我想讓每顆工作包有自己的建置產物與依賴,以便 A 分支裝的套件不會被 B 分支讀到。 3. 身為開發者,我想讓 agent 無論什麼時候讀檔都只讀到它那顆工作包的程式碼,以便不會產出接錯上下文、事後才發現的東西。 4. 身為開發者,我想讓工作樹的建立是一律的而非條件式的,以便不必自己判斷「這次會不會衝突」。 5. 身為開發者,我想在工作樹建立失敗時被明確擋下,以便不會以為自己在隔離環境裡、其實在原地改。 6. 身為開發者,我想讓工作樹集中在一個固定的家目錄底下,以便清理時只有一個地方要看。 7. 身為開發者,我不想讓工作樹出現在目標專案裡,以便 `git status` 不會多出東西、也不必改目標專案的忽略設定。 8. 身為開發者,我不想讓工作樹長在目標專案的父目錄,以便我自己的專案目錄結構不被這個工具塑形。 9. 身為開發者,我想讓工作樹路徑由工作包純函式推導,以便換機器或換 agent 之後仍然找得到同一棵。 10. 身為開發者,我想在推導出的工作樹不存在時自動重建,以便在新機器上接手不必手動補。 11. 身為開發者,我想讓新分支一律從遠端的來源分支長出,以便起點永遠是最新的、不會是落後好幾天的本機分支。 12. 身為開發者,我想在遠端沒有指定的來源分支時被明確擋下,以便不會靜默地退回一個錯的起點。 13. 身為開發者,我想讓建分支與建工作樹是同一個動作,以便失敗時不會留下有分支沒工作樹、或有工作樹沒分支的半成品。 14. 身為開發者,我想讓碼錶在工作樹建成之後才起動,以便一次失敗的領取不會在工時報表上留下一段憑空的時間。 15. 身為開發者,我想在工作樹建好時被告知它是乾淨的,以便知道要先裝依賴才能跑起來。 16. 身為開發者,我不想讓任何機密檔案被自動複製到工作樹,以便不會在意料之外的地方多出一份憑證。 17. 身為開發者,我想知道 PR 現在的狀態與還有幾則留言沒處理,以便決定要不要現在動手。 18. 身為開發者,我想讓這個查詢是一次性的、無狀態的,以便可以用我自己的排程去跑,而不是被迫開著一個終端機。 19. 身為開發者,我想讓查詢結果包含工作樹的路徑與是否有未提交變更,以便一眼看出能不能直接接著做。 20. 身為開發者,我想讓「建議動作」是固定的列舉值,以便我的排程能程式化判斷而不是去解讀一段文字。 21. 身為開發者,我不想讓任何流程在我沒下指令時自動改我的程式碼,以便我永遠知道是誰動的手。 22. 身為開發者,我想在 PR 合併或關閉時讓工作樹被自動清掉,以便集中目錄不會無限長大。 23. 身為開發者,我想在 PR 被退回草稿時**保留**工作樹,以便繼續改的時候東西還在。 24. 身為開發者,我想在工作樹裡還有未提交變更時讓清理被擋下,以便不會被自動流程刪掉還沒存的東西。 25. 身為開發者,我想在清理工作樹時保留本機與遠端分支,以便之後還能回頭看那段歷史。 26. 身為開發者,我想有一個手動清理的出口,以便處理那些永遠不會被合併也不會被關閉的 PR。 27. 身為開發者,我想讓 `sdlc-fix` 自己定位到正確的工作樹,以便處理留言時不必先手動 cd。 28. 身為開發者,我想讓 `sdlc-fix` 在我從來沒跑過監看腳本的情況下也能自己發現 PR 已經合併,以便不會在一棵該被清掉的工作樹上白做工。 29. 身為開發者,我想讓工作樹的隔離只管檔案、不管工時,以便工時報表的數字仍然是「這段時間你花在這件事上」。 30. 身為開發者,我想在因為碼錶還在跑而被擋下領取時,被明確告知停錶不會動到既有的工作樹,以便不會以為停錶等於放棄那顆工作包。 ## Implementation Decisions **工作樹路徑** 集中在 `~/.tea-sdlc/worktrees/{hash}`,其中 `hash` 為 `sha256` 對**正規化後**的 `owner/repo/分支名`(小寫、去前後空白)取值後的前 12 碼。 選集中式而非 repo 內的 `.worktrees/`:後者未被忽略時會出現在目標專案的 `git status`,而要它不出現就得改目標專案的忽略設定,那是 #1 的 Out of Scope 明文禁止的。選集中式而非 repo 的兄弟目錄:兄弟目錄會在使用者的專案父目錄長出一堆東西,而那個目錄結構屬於使用者,不屬於這個工具。 選雜湊而非把分支名的斜線攤平成 `-`:攤平會讓 `feat/a-b/main` 與 `feat/a/b/main` 撞成同一個目錄名,而既有的分支命名規則(`{類型}/{需求描述}/{功能描述}`)恰好讓這種形狀有機會出現。雜湊沒有可讀後綴,可讀性的缺口由兩處補上:`git worktree list` 的輸出本來就會把分支名印在路徑旁邊,而 PR 檢查腳本會回報推導出的路徑。 正規化是必要的——沒有它,同一棵工作樹會因為輸入大小寫或多一個空白而被推導成兩個不同路徑。 **必須明說的邊界**:`git worktree add` 一定會在目標 repo 的 `.git/worktrees/` 底下寫中繼資料,這是 git 的機制,無法避免。#1 的 Out of Scope 所說「不修改目標專案的檔案」指的是專案內容檔(`CLAUDE.md`、設定檔之類),不含 git 自己的內部中繼資料。 **建立流程** `git worktree add -b {新分支} {推導路徑} origin/{來源分支}` 是一個原子動作,同時建立分支與工作樹。之前先 `git fetch` 更新遠端引用——`git worktree add` 從一個既有的 ref 長出,來源不新則起點就不對。 遠端不存在指定的來源分支時**明確中止**,錯誤訊息指出「請先把來源分支推上去,或改指定一個已存在的來源分支」。不退回本機同名分支:那是靜默降級,而且後果隱蔽——使用者以為自己從最新的遠端狀態開工,實際上起點可能落後好幾天。 **不設 upstream**。此刻遠端還沒有這個新分支,`--track origin/{來源}` 會把 upstream 指到**來源分支**,之後 `git pull` 會把來源分支的新提交拉進來,幾乎一定不是使用者要的。upstream 留給 #13 的第一次 `push -u` 自然建立。 **起錶時機後移**:碼錶在工作樹建成之後才起動。#11 現行的隱含順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立失敗會中止——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,而工時要準正是 `/sdlc-report` 的立足點。 **工作樹裡的開發環境** 不複製也不連結 `node_modules`、`vendor`、建置快取。symlink 特別危險:兩棵工作樹共用同一份 `node_modules`,A 分支的安裝會改到 B 分支讀到的東西,正好把工作樹要隔離的東西又接回去,而且是以最難察覺的方式。複製則昂貴且立刻過期。 改為建立後印出提示,安裝指令借用 #12 已經要做的語言偵測產生(偵測到 `package.json` 提示 `npm install`、`composer.json` 提示 `composer install`,偵測不到就只說「這是一棵乾淨的工作樹」)。`.env` 這類機密**絕不自動複製**,只提示使用者自己放。 **PR 監看** 一支一次性的檢查腳本,具名 flag 進、單行 JSON 出、無狀態、冪等。不做常駐程序也不做 daemon:daemon 需要 pidfile,而 #11 明定「不在本機留任何狀態檔,換機器或換 agent 都能接手」;常駐前景程序雖然不留檔,卻把「監看中」這個狀態綁在一個終端機 session 上,同樣違反那條規則的精神。排程交給呼叫端(cron、或 agent 工具自己的循環機制),本工具不長出排程器。 **不做變化偵測**。「已處理」的判定基準是 #14 定下的 `+1` reaction,也就是這個狀態已經存在 Gitea 上、不在本機記憶裡,所以現況快照本身就足以回答「還有沒有事要做」。比較前後兩次結果是個不存在的需求。 回傳內容:PR 狀態(open/merged/closed/draft)、未處理留言數、工作樹現況(推導路徑、是否存在、有無未提交變更)、`terminal`、`cleaned`、以及列舉值形式的 `suggestedAction`(`run-sdlc-fix`/`cleanup`/`nothing-to-do`/`blocked-dirty`)。用列舉值而非自由文字,呼叫端才能程式化判斷。 **三種 PR 狀態的處置不一致**:merged 與 closed 是終止狀態,`terminal: true` 並自動清理工作樹;**draft 繼續監看且不清理**——被退回草稿代表還要繼續改,這時候使用者更需要那棵工作樹。 **監看只通知,不動手** 偵測到有未處理留言時只印出建議,不自動執行 `/sdlc-fix`。理由有兩條,都是硬的:#1 的使用者故事第 77 條明定「不想讓這些流程被模型自動觸發」;而 #14 的驗收標準要求「分類或做法不確定時詢問使用者,不自行決定」,在非互動模式下那個詢問無處可去,agent 只能自行決定,等於把一條驗收標準做成謊言。 唯一自動執行的副作用是清理工作樹,而那件事完全可逆(隨時能重建)。 `/sdlc-fix` 另外自行檢查 PR 狀態,補上「使用者從來沒跑過監看腳本」的缺口。 **清理範圍** 只 `git worktree remove`,**本機分支與遠端分支都保留**。遠端分支的刪除是 Gitea 合併時的選項,由使用者在網頁上決定,本工具代勞就越界(同 #1 Out of Scope 那條「不提供議題的刪除或關閉流程」的精神)。本機分支不佔什麼空間,留著讓使用者還能回頭看。 **絕不加 `--force`**:工作樹內有未提交變更時明確中止並報出路徑。這條之所以重要,是因為清理會被自動執行——而自動執行的東西只能做可逆的事。 **腳本歸屬** `branch-prep` 吸收工作樹建立(Q14 已定它與建分支是同一個原子動作,拆開必然有一支做半套)。新增 `pr-watch` 與 `worktree-remove` 兩支,共用同一份移除實作——`pr-watch` 直接呼叫共用函式,不另起子行程。`worktree-remove` 存在的意義是給使用者一個手動出口,處理那些永遠不會被合併也不會被關閉的 PR。#1 的腳本清單因此再長出這兩支。(實作後另由 #42 追加第三支 `worktree-ensure`:`/sdlc-fix` 要能由工作包定位工作樹、不在就重建,而那條路徑需要自己的入口。) **工時層不變** 工作樹解決的是**檔案層**的互相影響,不是工時層的。#1 的領取規則(自己碼錶跑在任何議題一律擋)維持不變:工時本來就該是序列的,一次只能把時間花在一件事上。 這條規則有個容易被誤認為冗餘的地方,要寫下來:**Gitea 在不同議題上起新錶時,會回 201 成功並靜默地停掉且記錄前一顆錶**(`HasUserStopwatch` 回傳單一碼錶,`CreateIssueStopwatch` 建立前先 `Finish` 既有的那顆;409 只發生在同一顆議題上重複起錶)。所以「自己碼錶跑在別的議題也擋」是本工具**刻意加的保護**——沒有它,`claim` 會靜默結算使用者上一段工時。 附帶要改的只有錯誤訊息:被碼錶擋下時要明說「停錶不會動到你既有的工作樹」,否則使用者會以為停錶等於放棄那顆工作包。 ## Testing Decisions **什麼是好的測試**:只測外部行為。對本需求而言,外部行為是「給定一組 flag 與一份 Gitea 回應,腳本印出什麼 JSON、對 Gitea 發出哪些請求、以及對 git 做了什麼」。 **接縫維持既有的那一個**:`scripts/*.js` 的 CLI 邊界,以子行程執行、比對 stdout 的 JSON 與 exit code。不新增接縫。 **git 操作不做 mock**:沿用既有做法,在臨時 git repo 上跑真實 git。工作樹是 git 的功能,用假的 git 測工作樹等於什麼都沒測。臨時 repo 需要一個「遠端」才能測 `origin/{來源分支}`,以本機裸 repo 充當即可,不需要網路。 **測試對象優先序**: 1. **工作樹路徑推導** —— 純字串與雜湊運算,表格驅動,成本最低回歸價值最高。要涵蓋:大小寫與空白的正規化(同一輸入的變體必須推導出同一路徑)、分支名含斜線、`feat/a-b/main` 與 `feat/a/b/main` 不撞名。 2. **建立流程的失敗路徑** —— 遠端不存在來源分支時中止且**不留下任何分支或工作樹**;工作樹已存在時的行為;起錶確實發生在工作樹建成之後(失敗案例中不得有起錶請求)。 3. **清理的守門** —— 工作樹內有未提交變更時中止並報路徑,且工作樹仍然存在;乾淨時移除成功且本機分支仍在。 4. **PR 狀態到處置的對照表** —— open/merged/closed/draft 四種狀態各自的 `terminal`、`cleaned`、`suggestedAction`,特別是 draft 必須 `terminal: false, cleaned: false`。 5. **`--dry-run`** —— 所有寫入型操作在 `--dry-run` 下輸出「將執行的 git 指令與將發出的請求」而不實際執行。 **Prior art**:既有的腳本契約測試已用子行程加 stub server 驗過腳本契約與四層前置檢查,臨時 git repo 的 helper 也已存在,兩者都可直接沿用。測試執行器維持 Node 內建的測試器,暫存一律寫到已被忽略的暫存目錄。 ## Out of Scope - 不處理多人協作:工作樹只隔離「同一個人、同一份 clone」的多顆工作包。兩個人各自 clone 本來就互不影響。 - 不放寬碼錶規則,不支援同時計時多顆工作包。 - **不自動執行 `/sdlc-fix`**,即使監看腳本已經偵測到有未處理的留言。 - 不複製、不連結 `node_modules`、`vendor`、建置快取或任何機密檔案到工作樹。 - 不刪除本機分支,不刪除遠端分支。 - 不以 `--force` 移除含未提交變更的工作樹。 - 不提供常駐程序、daemon 或任何形式的排程器。 - 不修改目標專案的忽略設定或任何專案內容檔。 - 不修改 #1、#11、#14 三顆既有議題。 ## Further Notes - 目前 repo 內完全沒有任何工作樹相關實作或提及,這是全新的一條分支,不是修改既有行為。 - 本機 git 版本 2.47.3,工作樹功能完整支援。 - 相依關係:本需求的工作包 A 掛在 #11 之下,B+C 與 D 掛在 #14 之下並同時被 A 擋住。A 是 #11 的旁支,不在 #11→#12→#13→#14 那條主鏈上,所以 #14 完成**不蘊含** A 完成——`A → B+C` 與 `A → D` 這兩條邊是真實的實作順序限制,不是為了好看。 - 已知風險(已被接受):#11 為 open 且 `ready-for-agent`,任何人或 agent 領走它讀到的仍是被本議題取代的條文。緩解方式是本議題的「取代宣告」段,但它只能被讀到本議題的人看到。
jiantw83 added the ready-for-agent label 2026-09-17 07:11:13 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: plugins/tea-sdlc#38