以 worktree 建立工作包分支並備妥隔離環境 #46

Merged
admin merged 5 commits from feat/worktree-branch-prep/main into master 2026-09-17 08:31:54 +00:00
Member

摘要

領取工作包時不再在原地切換分支,而是一律建立一棵屬於這顆工作包的獨立工作樹。
分支與工作樹合併為同一個原子動作,起點一律取自遠端,碼錶則後移到工作樹建成之後才起。

需求議題

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

工作包議題

#40 — 以 worktree 建立工作包分支並備妥隔離環境

變更內容

  • scripts/branch-prep.js:改寫。git fetch origin 後以單一
    git worktree add --no-track -b {新分支} {推導路徑} origin/{來源分支} 完成;
    失敗時回滾分支與目錄;建好後回報乾淨工作樹與偵測到的安裝指令。新增必填 --repo。
  • scripts/lib.js:新增 worktreePath(路徑推導)、listStopwatches/stopwatchOnIssue/
    stopwatchElsewhere(三支腳本共用的碼錶讀取與擋路訊息)。
  • scripts/timer.js:新增。起錶,且已在計時時什麼都不做。
  • scripts/claim.js:拿掉起錶,只留領取鎖的 assignee 與標籤。
  • scripts/wp-extract.js:碼錶讀取改用 lib 的共用函式。
  • prompts/sdlc-feat.md:第一段改成「讀 → 領 → 備妥工作樹 → 起錶」,共七步;
    第一行 description 的順序跟著改。第二段補上「改的是工作樹裡的檔案」,
    第三段 commit-split --path 指向工作樹,兩段的步驟編號各往後移一號。
  • AGENTS.md:補上工作樹的集中位置,以及 .git/worktrees/ 這條 git 自己的例外。
  • 測試:test/worktree-path.test.js(表格驅動)、test/timer.test.js 為新增,
    test/branch-prep.test.js 改寫,claim 與 sdlc-feat-assets 跟著調整。

設計重點

為什麼工作樹是一律的,不是「有衝突才用」。 原地切分支有三種損耗:未提交的變更
擋路、建置產物跨分支混淆,以及 agent 讀到不屬於它那顆工作包的程式碼。前兩種人會
當場發現,第三種不會——agent 是非同步的,它可能在分支已經被切走之後才去讀檔,
而且不會察覺,產出看起來完全合理,只是接錯了上下文。

為什麼一定要 --no-track。 這不是理論問題:開這棵工作樹時 git 就自動把 upstream
設成了 origin/master。此刻遠端還沒有這支新分支,upstream 只會落在來源分支上,
之後 git pull 會把來源分支的提交拉進來。upstream 留給第一次 push -u 自然建立。

為什麼要回滾。 實測 git worktree add 失敗時仍會把新分支留下來。那是最難查的
半成品:下一次重跑會走到「目標分支已存在」那條路,起點從此不再是遠端的來源分支。
測試以「家目錄被一個檔案擋住」造出這個情境,並反向驗證過——拿掉回滾就紅。

為什麼路徑取雜湊而不是把斜線攤平。 攤平會讓 feat/a-b/main 與 feat/a/b/main
撞成同一個目錄,而現行的分支命名規則恰好讓這種形狀有機會出現。

為什麼起錶要後移。 工作樹建立失敗會中止整個領取。錶要是照舊在領取那一步就起,
使用者會被計一段什麼都沒做的時間,而工時要準正是 /sdlc-report 的立足點。

工作區不乾淨不再是阻擋條件。 DIRTY_WORKTREE 整條拿掉——讓未提交的變更不再擋路,
本來就是工作樹要解決的事。

解決的問題

同時持有多顆工作包的人不必再為了換一顆而先處理未提交的變更;每顆工作包有自己的
依賴與建置產物;agent 無論什麼時候讀檔都只讀得到它那顆工作包的程式碼。
另外,被碼錶擋下來時會明說停錶不會動到既有的工作樹(#38 使用者故事第 30 條)——
以為停錶等於放棄工作包的人會寧可不停,工時就記到別顆去了。

影響的功能

/sdlc-feat 第一段,以及第二、三段的工作目錄——它們的 --path 與實作動作都改指工作樹。branch-prep 的介面有兩處不相容的變更:新增必填 --repo,
以及不再有 DIRTY_WORKTREE。正本已同步,沒有其他呼叫端。

#11 驗收標準第 4 條(來源分支在遠端已存在時 pull 而非重建)依 #38 由本工作包取代,
#11 本身不修改;第 5 條的分支命名規則不變。

測試結果

npm test 633 pass / 0 fail(已 rebase 到含 #45 的 master 之上)。工作樹相關的測試都在臨時 git repo 上跑真的 git,
以本機裸 repo 充當遠端,不需網路。

另外對真實 repo 試跑過一次:

node scripts/branch-prep.js --repo plugins/tea-sdlc --path /root/plugins/tea-sdlc \
  --source master --type feat --slug smoke-test --dry-run
→ git fetch origin
  git worktree add --no-track -b feat/smoke-test/main ~/.tea-sdlc/worktrees/72765a939540 origin/master

🤖 Generated with Claude Code

## 摘要 領取工作包時不再在原地切換分支,而是一律建立一棵屬於這顆工作包的獨立工作樹。 分支與工作樹合併為同一個原子動作,起點一律取自遠端,碼錶則後移到工作樹建成之後才起。 ## 需求議題 #38 — 以 worktree 隔離平行工作包的實作與修正 ## 工作包議題 #40 — 以 worktree 建立工作包分支並備妥隔離環境 ## 變更內容 - `scripts/branch-prep.js`:改寫。`git fetch origin` 後以單一 `git worktree add --no-track -b {新分支} {推導路徑} origin/{來源分支}` 完成; 失敗時回滾分支與目錄;建好後回報乾淨工作樹與偵測到的安裝指令。新增必填 `--repo`。 - `scripts/lib.js`:新增 `worktreePath`(路徑推導)、`listStopwatches`/`stopwatchOnIssue`/ `stopwatchElsewhere`(三支腳本共用的碼錶讀取與擋路訊息)。 - `scripts/timer.js`:新增。起錶,且已在計時時什麼都不做。 - `scripts/claim.js`:拿掉起錶,只留領取鎖的 assignee 與標籤。 - `scripts/wp-extract.js`:碼錶讀取改用 lib 的共用函式。 - `prompts/sdlc-feat.md`:第一段改成「讀 → 領 → 備妥工作樹 → 起錶」,共七步; 第一行 description 的順序跟著改。第二段補上「改的是工作樹裡的檔案」, 第三段 `commit-split --path` 指向工作樹,兩段的步驟編號各往後移一號。 - `AGENTS.md`:補上工作樹的集中位置,以及 `.git/worktrees/` 這條 git 自己的例外。 - 測試:`test/worktree-path.test.js`(表格驅動)、`test/timer.test.js` 為新增, `test/branch-prep.test.js` 改寫,`claim` 與 `sdlc-feat-assets` 跟著調整。 ## 設計重點 **為什麼工作樹是一律的,不是「有衝突才用」。** 原地切分支有三種損耗:未提交的變更 擋路、建置產物跨分支混淆,以及 agent 讀到不屬於它那顆工作包的程式碼。前兩種人會 當場發現,第三種不會——agent 是非同步的,它可能在分支已經被切走之後才去讀檔, 而且不會察覺,產出看起來完全合理,只是接錯了上下文。 **為什麼一定要 `--no-track`。** 這不是理論問題:開這棵工作樹時 git 就自動把 upstream 設成了 `origin/master`。此刻遠端還沒有這支新分支,upstream 只會落在來源分支上, 之後 `git pull` 會把來源分支的提交拉進來。upstream 留給第一次 `push -u` 自然建立。 **為什麼要回滾。** 實測 `git worktree add` 失敗時仍會把新分支留下來。那是最難查的 半成品:下一次重跑會走到「目標分支已存在」那條路,起點從此不再是遠端的來源分支。 測試以「家目錄被一個檔案擋住」造出這個情境,並反向驗證過——拿掉回滾就紅。 **為什麼路徑取雜湊而不是把斜線攤平。** 攤平會讓 `feat/a-b/main` 與 `feat/a/b/main` 撞成同一個目錄,而現行的分支命名規則恰好讓這種形狀有機會出現。 **為什麼起錶要後移。** 工作樹建立失敗會中止整個領取。錶要是照舊在領取那一步就起, 使用者會被計一段什麼都沒做的時間,而工時要準正是 `/sdlc-report` 的立足點。 **工作區不乾淨不再是阻擋條件。** `DIRTY_WORKTREE` 整條拿掉——讓未提交的變更不再擋路, 本來就是工作樹要解決的事。 ## 解決的問題 同時持有多顆工作包的人不必再為了換一顆而先處理未提交的變更;每顆工作包有自己的 依賴與建置產物;agent 無論什麼時候讀檔都只讀得到它那顆工作包的程式碼。 另外,被碼錶擋下來時會明說停錶不會動到既有的工作樹(#38 使用者故事第 30 條)—— 以為停錶等於放棄工作包的人會寧可不停,工時就記到別顆去了。 ## 影響的功能 `/sdlc-feat` 第一段,以及第二、三段的工作目錄——它們的 `--path` 與實作動作都改指工作樹。`branch-prep` 的介面有兩處不相容的變更:新增必填 `--repo`, 以及不再有 `DIRTY_WORKTREE`。正本已同步,沒有其他呼叫端。 `#11` 驗收標準第 4 條(來源分支在遠端已存在時 pull 而非重建)依 #38 由本工作包取代, #11 本身不修改;第 5 條的分支命名規則不變。 ## 測試結果 `npm test` 633 pass / 0 fail(已 rebase 到含 #45 的 master 之上)。工作樹相關的測試都在臨時 git repo 上跑真的 git, 以本機裸 repo 充當遠端,不需網路。 另外對真實 repo 試跑過一次: node scripts/branch-prep.js --repo plugins/tea-sdlc --path /root/plugins/tea-sdlc \ --source master --type feat --slug smoke-test --dry-run → git fetch origin git worktree add --no-track -b feat/smoke-test/main ~/.tea-sdlc/worktrees/72765a939540 origin/master 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jiantw83 added 5 commits 2026-09-17 08:30:06 +00:00
同一份 clone 上同時持有多顆工作包時,原地切分支有三種損耗,一種比一種難查:
未提交的變更擋路、建置產物跨分支混淆,以及 agent 讀到不屬於它那顆工作包的
程式碼——agent 是非同步的,它可能在分支已經被切走之後才去讀檔,而且不會察覺,
產出看起來完全合理,只是接錯了上下文。前兩種人會當場發現,第三種不會,
所以工作樹一律建立,不是「有衝突才用」。

建不起來就中止,不退回原地切分支:靜默降級會讓使用者以為自己在隔離環境裡,
其實在原地改。

分支與工作樹合併為一個原子動作(fetch 後一次 worktree add),並補上回滾——
git 在 worktree add 失敗時仍會把分支留下來,那是最難查的半成品:下一次重跑
會走到「目標分支已存在」那條路,起點從此不再是遠端的來源分支。

起點一律取自 origin/{來源分支},遠端沒有就中止,不退回本機同名分支;
本機分支可能落後好幾天,而這件事從輸出上完全看不出來。原「來源分支在遠端
已存在時 pull 而非重建」那條,用更強的方式達成同一個目的:根本不碰本機分支,
就沒有覆蓋他人進度的可能。

不設 upstream:此刻遠端還沒有這個新分支,--track 會把 upstream 指到來源分支,
之後 git pull 會把來源分支的提交拉進來。留給第一次 push -u 自然建立。

路徑由 owner/repo/分支名 正規化後取雜湊推導(lib 的 worktreePath),不查表、
不寫狀態檔,換機器算出來一樣。取雜湊而不是把斜線攤平成 -,是因為攤平會讓
feat/a-b/main 與 feat/a/b/main 撞成同一個目錄,而現行的分支命名規則恰好讓
這種形狀有機會出現。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
原本的順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立
失敗會中止整個領取——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,
而工時要準正是工時報表的立足點。

claim 只留領取鎖的兩件事(assignee 與標籤),起錶交給新的 timer.js,由流程
正本排在 branch-prep 之後。timer 已經跑在這顆議題上時什麼都不做:中斷後重跑
是它最常見的處境,重新起錶會把已經累積的時間切成兩段;跑在別顆上則照舊擋下,
不代勞停錶。

三支腳本讀碼錶的那段各留一份,趁這次收進 lib。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
正本跟著實作走:分支那一步改成工作樹,新增起錶那一步排在它後面,
並把三處「不要自己決定」寫明——來源分支不存在時不自己換一支、路徑被佔住時
不自己刪、工作樹建不起來時不退回原地切分支。工作區不乾淨的處置整段拿掉:
工作樹本來就是為了讓未提交的變更不再擋路。

AGENTS.md 補上兩條邊界。git worktree add 一定會在目標 repo 的 .git/worktrees/
底下寫中繼資料,這是 git 的機制,無法避免——「不改目標專案」指的是專案的內容檔,
把這件事明說,免得下一個人以為實作違規。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
議題 #38 的使用者故事第 30 條:使用者常以為停錶等於放棄那顆工作包,於是寧可
不停——工時就記到別顆議題去了。碼錶只管時間、工作樹只管檔案,兩者互不相干,
這件事要在擋下來的當下就講,不能指望使用者自己推論。

領取與起錶會撞到同一個擋路理由,訊息收進 lib 只寫一份。順手收掉 review 指出的
三處:planWorktree 沒用到的 repo 參數、與 path.resolve 同名而誤導的區域函式、
以及只有 lib 自己用得到卻對外 export 的兩支路徑函式。

回滾補上最後一道:git 清不掉時把目錄本身也刪掉。那條路徑在這次執行之前不存在
(不存在正是建立的前提),裡面不可能有使用者的東西,而留著它下一次重跑會直接
撞上 WORKTREE_PATH_TAKEN——一次失敗的建立不該讓人從此開不了工。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
轉接檔的一行說明是從這裡取的,順序寫錯會讓人以為錶還是在領取那一步起。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jiantw83 force-pushed feat/worktree-branch-prep/main from dbce8dbd3c to d3ce364727 2026-09-17 08:30:06 +00:00 Compare
admin approved these changes 2026-09-17 08:31:51 +00:00
admin merged commit 3084c91041 into master 2026-09-17 08:31:54 +00:00
admin deleted branch feat/worktree-branch-prep/main 2026-09-17 08:31:54 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: plugins/tea-sdlc#46