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