docs(sdlc-feat): 第一段改成領取、備妥工作樹、最後起錶
正本跟著實作走:分支那一步改成工作樹,新增起錶那一步排在它後面, 並把三處「不要自己決定」寫明——來源分支不存在時不自己換一支、路徑被佔住時 不自己刪、工作樹建不起來時不退回原地切分支。工作區不乾淨的處置整段拿掉: 工作樹本來就是為了讓未提交的變更不再擋路。 AGENTS.md 補上兩條邊界。git worktree add 一定會在目標 repo 的 .git/worktrees/ 底下寫中繼資料,這是 git 的機制,無法避免——「不改目標專案」指的是專案的內容檔, 把這件事明說,免得下一個人以為實作違規。 議題 #40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,4 +30,10 @@
|
||||
- **契約以議題為正本**:腳本的 flag 介面、JSON 輸出形狀、前置檢查與路徑定位規則,正本在[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),實作時以該處為準;本檔不複寫,以免兩邊走鐘。
|
||||
- **測試**:`npm test`(等同 `node --test`)。測試產生的暫存一律寫到 `.tmp/`,該目錄已被 git 忽略,也不會被測試探索掃到。
|
||||
- **不改目標專案**:本 plugin 只讀目標專案的程式碼,不寫入目標專案的 `CLAUDE.md` 或任何設定檔。
|
||||
唯一的例外是 git 自己的內部中繼資料——`git worktree add` 一定會在目標 repo 的
|
||||
`.git/worktrees/` 底下寫東西,那是 git 的機制,無法避免,也不是專案的內容檔。
|
||||
- **工作樹集中在家目錄**:每顆工作包的工作樹開在 `~/.tea-sdlc/worktrees/{hash}`,
|
||||
路徑由 `owner/repo/分支名` 純函式推導(`scripts/lib.js` 的 `worktreePath`),
|
||||
不查表也不寫狀態檔。不開在目標專案裡(會出現在它的 `git status`),
|
||||
也不開在它的兄弟目錄(那個目錄結構屬於使用者)。
|
||||
- **不自動觸發**:所有指令僅由使用者明確叫用;skill/command 的 `description` 統一以「僅由 /sdlc-xxx 指令叫用。」起頭。
|
||||
|
||||
+67
-24
@@ -5,7 +5,7 @@ description: 僅由 /sdlc-feat 指令叫用。領取一顆工作包、起錶、
|
||||
|
||||
拿一顆工作包,從領取到開出 PR。
|
||||
|
||||
第一段**領取與開工準備**:把工作包安全地認領下來,開始計時,備妥開工的分支。
|
||||
第一段**領取與開工準備**:把工作包安全地認領下來,備妥一棵屬於它的工作樹,然後開始計時。
|
||||
這一段不改任何一行程式碼——它只負責讓後面的實作有個乾淨的起點。
|
||||
|
||||
第二段**逐項實作**:一項一項把待辦做完並即時勾選,讓議題頁的進度條隨時反映真實狀態。
|
||||
@@ -45,8 +45,9 @@ node scripts/wp-extract.js --repo <owner/name> --index <編號>
|
||||
node scripts/claim.js --repo <owner/name> --index <編號> --dry-run
|
||||
```
|
||||
|
||||
確認無誤後拿掉旗標再跑一次。放行時它會設 assignee、貼「進行中」標籤、起錶——三件事
|
||||
一起構成領取鎖,錶則是工時的來源。
|
||||
確認無誤後拿掉旗標再跑一次。放行時它會設 assignee、貼「進行中」標籤——這兩件事一起
|
||||
構成領取鎖。**錶不在這一步起**:它等工作樹建好之後才起(第 6 步)。工作樹建立失敗會
|
||||
中止整個領取,錶要是先起了,使用者就被計了一段什麼都沒做的時間。
|
||||
|
||||
領取鎖有四種狀態,三種擋、一種放行。被擋下來時**不要繞過去**,照著錯誤碼告訴使用者
|
||||
發生什麼事、下一步是什麼:
|
||||
@@ -72,6 +73,8 @@ node scripts/claim.js --repo <owner/name> --index <編號> --dry-run
|
||||
但若這顆工作包明顯是某個既有功能分支的一部分,就建議那一支,並說明為什麼。
|
||||
- **手動輸入** — 讓使用者自己填分支名。
|
||||
|
||||
不論哪一種,來源分支都必須**已經在遠端上**:工作樹的起點一律取自 `origin/{來源分支}`。
|
||||
|
||||
### 4. 把議題標題翻成英文
|
||||
|
||||
分支名的中段要用英文,中文會讓 CI 與 URL 出問題。把工作包議題的標題翻成
|
||||
@@ -79,41 +82,71 @@ node scripts/claim.js --repo <owner/name> --index <編號> --dry-run
|
||||
|
||||
翻譯要保留原意而不是逐字直譯,寧可用一個更短的說法,也不要把長句截斷成看不懂的字串。
|
||||
|
||||
### 5. 備妥分支
|
||||
### 5. 備妥工作樹
|
||||
|
||||
分支開在**工作包的 `repos` 列出的那些 repo** 上,不是開在本 plugin 的目錄裡。
|
||||
工作樹開在**工作包的 `repos` 列出的那些 repo** 上,不是開在本 plugin 的目錄裡。
|
||||
`repos` 只有一顆就用那一顆;**有多顆時逐一確認**要在哪幾個開分支,
|
||||
再對每一個各跑一次 `branch-prep`,分支名在每個 repo 都相同。
|
||||
|
||||
```
|
||||
node scripts/branch-prep.js --path <目標專案路徑> --source <來源分支> \
|
||||
--slug <英文-kebab> [--type feat] --dry-run
|
||||
node scripts/branch-prep.js --repo <owner/name> --path <目標專案路徑> \
|
||||
--source <來源分支> --slug <英文-kebab> [--type feat] --dry-run
|
||||
```
|
||||
|
||||
`--type` 只在來源是開發分支時要給(`feat`/`fix`/`chore`…);從功能分支長出時,
|
||||
類型與需求描述沿用來源,不必也不能再指定。
|
||||
|
||||
試跑會印出將執行的 git 指令與算出來的分支名。確認無誤後拿掉旗標再跑一次。
|
||||
試跑會印出將執行的 git 指令、算出來的分支名與工作樹路徑。確認無誤後拿掉旗標再跑一次。
|
||||
|
||||
它保證三件事,都是為了不弄丟別人的東西:工作區不乾淨時**先擋下來**,免得把不相干的
|
||||
改動帶進這顆工作包的分支;來源分支在遠端已存在時是 **pull 而不是重建**;目標分支已經
|
||||
存在時是**接上去而不是蓋掉**。
|
||||
**不在原地切換分支,一律開一棵獨立的工作樹。** 每顆工作包有自己的目錄、自己的建置
|
||||
產物、自己的未提交變更,彼此看不見對方。這件事對 agent 特別重要:它是非同步的,
|
||||
可能在分支已經被切走之後才去讀檔,而它**不會察覺**自己讀到的是別顆工作包的內容——
|
||||
產出看起來完全合理,只是接錯了上下文。
|
||||
|
||||
工作區不乾淨(`DIRTY_WORKTREE`)時,把 git 回報的檔案念給使用者聽,讓他決定要提交、
|
||||
`git stash` 還是丟掉——**不要自己選**。
|
||||
**工作樹一律建立,沒有例外。** 建不起來就照實中止,**不要改成在原地切分支**:
|
||||
使用者會以為自己在隔離環境裡,其實在原地改。
|
||||
|
||||
### 6. 回報
|
||||
工作樹路徑由 `owner/repo/分支名` 推導而得,印在輸出的 `worktree` 欄位。
|
||||
**後面幾段的實作、測試與提交都在那棵工作樹裡做**,不要回到主工作區動手。
|
||||
|
||||
它保證三件事:
|
||||
|
||||
- **起點一律是遠端的來源分支**(`origin/{來源分支}`),不是本機同名分支——後者可能
|
||||
落後好幾天。遠端沒有那一支時得到 `SOURCE_NOT_FOUND`,把訊息念給使用者,讓他決定
|
||||
是先把來源分支推上去,還是改指定一個別的來源——**不要自己換一個**。
|
||||
- **目標分支已經存在時接上去而不是蓋掉**;工作樹已經在了就沿用,不動裡面還沒提交的東西。
|
||||
- **失敗時不留半成品**:不會出現有分支沒工作樹、或有工作樹沒分支的狀態。
|
||||
|
||||
推導出的路徑被別的東西佔住時(`WORKTREE_PATH_TAKEN`,多半是別的 clone 留下的),
|
||||
把路徑念給使用者,請他確認裡面沒有還沒保存的東西再移除——**不要自己刪**。
|
||||
|
||||
工作樹是乾淨的:**沒有安裝依賴,也沒有任何建置產物**,`.env` 這類機密檔案更不會被
|
||||
複製過去。把輸出的 `提示.安裝指令` 念給使用者,機密檔案請他自己放一份。
|
||||
|
||||
### 6. 起錶
|
||||
|
||||
工作樹建好之後才起錶:
|
||||
|
||||
```
|
||||
node scripts/timer.js --repo <owner/name> --index <編號> --dry-run
|
||||
```
|
||||
|
||||
確認無誤後拿掉旗標再跑一次。錶已經跑在這顆議題上時它什麼都不做——那正是中斷後重跑
|
||||
的情形,重新起錶會把已經累積的時間切成兩段。
|
||||
|
||||
### 7. 回報
|
||||
|
||||
印出一份開工前的現況,不寫回議題:
|
||||
|
||||
- 工作包標題與網址、這一顆有幾項待辦
|
||||
- 認領結果(是否本來就是自己的)、碼錶已起
|
||||
- 來源分支、新分支名、分支是新建還是接上既有
|
||||
- 工作樹路徑,以及它是乾淨的、要先跑哪一行安裝指令
|
||||
- 未處理留言數與未關閉的先決議題(若有)
|
||||
|
||||
## 第二段:逐項實作
|
||||
|
||||
### 7. 認出語言,讀規則正本
|
||||
### 8. 認出語言,讀規則正本
|
||||
|
||||
改任何一個檔案之前,先依專案檔認出這是什麼語言,再讀兩份規則正本:
|
||||
|
||||
@@ -127,7 +160,10 @@ node scripts/branch-prep.js --path <目標專案路徑> --source <來源分支>
|
||||
屬性的資料範例**優先從 MCP 取得**;取不到就以邏輯推理,並照 `comment-styles.md` 的寫法
|
||||
在註解裡註明「由邏輯推理、未經驗證」。這句註明不能省,否則後面的人會照著沒對過的格式寫解析。
|
||||
|
||||
### 8. 一項一項做
|
||||
### 9. 一項一項做
|
||||
|
||||
**改的是工作樹裡的檔案**,路徑就是 `branch-prep` 印出來的 `worktree`,不是主工作區——
|
||||
主工作區可能停在別的分支上,在那裡動手會把改動落到別顆工作包的分支去。
|
||||
|
||||
依 `wp-extract` 給的 `待辦` 順序做。每一項的做法:
|
||||
|
||||
@@ -141,7 +177,7 @@ node scripts/branch-prep.js --path <目標專案路徑> --source <來源分支>
|
||||
真正需要停下來問的只有三種:語言認不出來、待辦的意思有歧義、做下去會超出工作包的
|
||||
`範圍邊界`。除此之外一路做完。
|
||||
|
||||
### 9. 做完一項就勾一項
|
||||
### 10. 做完一項就勾一項
|
||||
|
||||
```
|
||||
node scripts/issue-update.js --repo <owner/name> --index <編號> \
|
||||
@@ -166,7 +202,7 @@ node scripts/issue-update.js --repo <owner/name> --index <編號> \
|
||||
**不要為了勾選在議題上留留言。** 勾選改的是 body,進度條自己會動;逐項留言會把議題洗版,
|
||||
reviewer 得從一堆「已完成第 N 項」裡找真正的討論。
|
||||
|
||||
### 10. 中斷後重跑
|
||||
### 11. 中斷後重跑
|
||||
|
||||
進度完全由 Gitea 上的勾選狀態推導,**不看任何本機檔案**。重跑這一段時:
|
||||
|
||||
@@ -175,7 +211,7 @@ reviewer 得從一堆「已完成第 N 項」裡找真正的討論。
|
||||
3. 已經勾過的項目再 `--tick` 一次是安靜的 no-op(回傳 `已經勾過: true`,不發 PATCH),
|
||||
所以不確定某一項有沒有勾到時,直接再勾一次即可,不必先查。
|
||||
|
||||
### 11. 回報
|
||||
### 12. 回報
|
||||
|
||||
全部待辦完成後印一份小結,不寫回議題:
|
||||
|
||||
@@ -187,15 +223,17 @@ reviewer 得從一堆「已完成第 N 項」裡找真正的討論。
|
||||
|
||||
## 第三段:提交與開立 PR
|
||||
|
||||
### 12. 分批提交
|
||||
### 13. 分批提交
|
||||
|
||||
全部待辦都勾完之後才進這一段。變更依類型分批:
|
||||
|
||||
```
|
||||
node scripts/commit-split.js --path <目標專案路徑> --type feat \
|
||||
node scripts/commit-split.js --path <工作樹路徑> --type feat \
|
||||
--subject '<繁中描述>' [--scope <功能名>] --dry-run
|
||||
```
|
||||
|
||||
`--path` 給的是第一段建出來的那棵**工作樹**——commit 要落在它的分支上。
|
||||
|
||||
`--type` 是**這次程式碼變更**的類型(`feat`/`fix`/`refactor`…);測試、文件與設定檔
|
||||
由腳本自己認出來,各自成批,不必也不能指定。`--body` 寫「為什麼這樣做」,那一段會接在
|
||||
每一顆 commit 的首行之後——本 repo 的歷史靠它讀得懂。
|
||||
@@ -217,7 +255,7 @@ node scripts/commit-split.js ... --files scripts/claim.js,test/claim.test.js
|
||||
|
||||
一顆 commit 的描述只說得清楚一件事,硬湊在一起就失去了分批的意義。
|
||||
|
||||
### 13. 寫 PR 描述
|
||||
### 14. 寫 PR 描述
|
||||
|
||||
固定八個段落,順序不能換——reviewer 每次都在同一個位置找到要找的資訊:
|
||||
|
||||
@@ -237,7 +275,7 @@ node scripts/commit-split.js ... --files scripts/claim.js,test/claim.test.js
|
||||
`pr-create` 會擋下缺段落、順序不對、以及測試結果只有空話的描述。被擋下來時**補真的內容**,
|
||||
不要為了通過而拼湊。
|
||||
|
||||
### 14. 開 PR 並停錶
|
||||
### 15. 開 PR 並停錶
|
||||
|
||||
```
|
||||
node scripts/pr-create.js --repo <目標專案 owner/name> --head <分支名> \
|
||||
@@ -258,7 +296,7 @@ repo**(錶停在那裡)。兩者常常不是同一個——議題在需求
|
||||
重跑不會開出第二顆 PR:同一個 head 已經有開著的 PR 就回傳它(`created` 為 `false`),
|
||||
然後照樣停錶——那一步可能正是上次中斷的地方。
|
||||
|
||||
### 15. 回報
|
||||
### 16. 回報
|
||||
|
||||
- PR 的網址與編號、標題(等同分支名),以及它是這次新開的還是接上既有的
|
||||
- 建立了哪幾顆 commit
|
||||
@@ -270,6 +308,11 @@ repo**(錶停在那裡)。兩者常常不是同一個——議題在需求
|
||||
- 第一段**不改任何一行程式碼**、不勾待辦、不提交、不開 PR——那些是後面幾段的事。
|
||||
- 第二段只實作與勾選。**不提交、不開 PR、不停錶**——那是第三段的事。
|
||||
- 第三段不改任何一行程式碼。到這裡實作已經結束,要改就回第二段改完再來。
|
||||
- **不在主工作區動手。** 第二段與第三段的每一個動作都在 `branch-prep` 建出來的那棵
|
||||
工作樹裡進行,包含跑測試與 `--path`。
|
||||
- 不把依賴、建置產物或 `.env` 這類機密檔案複製到工作樹裡,也不做連結——
|
||||
兩棵工作樹共用同一份依賴,正好把工作樹要隔離的東西又接回去。
|
||||
- 工作樹建不起來時中止,**不退回原地切分支**。
|
||||
- 不把「已測試通過」這種空話寫進 PR 描述,也不為了通過檢查而拼湊內容。
|
||||
- 不代替使用者決定 commit 的類型與描述;`--type` 與 `--subject` 都要是這次真的做了什麼。
|
||||
- 不把實作規範或註解格式寫進目標專案的任何檔案。
|
||||
|
||||
@@ -18,11 +18,37 @@ test('正本平台中立,description 前綴正確', () => {
|
||||
assertNeutralPrompt(prompt, 'sdlc-feat');
|
||||
});
|
||||
|
||||
test('第一段指名三支腳本,順序為先讀再領再備分支', () => {
|
||||
const order = ['wp-extract.js', 'claim.js', 'branch-prep.js'];
|
||||
test('第一段指名四支腳本,順序為先讀再領、備妥工作樹、最後起錶', () => {
|
||||
const order = ['wp-extract.js', 'claim.js', 'branch-prep.js', 'timer.js'];
|
||||
const positions = order.map((name) => phase1.indexOf(name));
|
||||
assert.equal(positions.every((p) => p >= 0), true, '三支腳本都要被指名');
|
||||
assert.deepEqual([...positions].sort((a, b) => a - b), positions, '領取之前要先讀得懂這顆在做什麼');
|
||||
assert.equal(positions.every((p) => p >= 0), true, '四支腳本都要被指名');
|
||||
assert.deepEqual(
|
||||
[...positions].sort((a, b) => a - b),
|
||||
positions,
|
||||
'領取之前要先讀得懂這顆在做什麼;錶則要等工作樹建好之後才起',
|
||||
);
|
||||
});
|
||||
|
||||
test('錶等工作樹建好之後才起,並說明為什麼', () => {
|
||||
assert.match(phase1, /錶不在這一步起/, '領取那一步要明講錶還沒起');
|
||||
assert.match(phase1, /什麼都沒做的時間/, '要說明為什麼後移:失敗的領取不該留下憑空的工時');
|
||||
});
|
||||
|
||||
test('工作樹一律建立,建不起來就中止而不是退回原地切分支', () => {
|
||||
assert.match(phase1, /一律建立,沒有例外/);
|
||||
assert.match(phase1, /不要改成在原地切分支/);
|
||||
assert.match(phase1, /以為自己在隔離環境裡/, '要說明靜默降級的後果');
|
||||
});
|
||||
|
||||
test('實作要在工作樹裡做,路徑從輸出取得', () => {
|
||||
assert.match(phase1, /worktree/, '工作樹路徑印在哪個欄位要講');
|
||||
assert.match(phase1, /都在那棵工作樹裡做/);
|
||||
});
|
||||
|
||||
test('工作樹是乾淨的,且機密檔案不會被複製過去', () => {
|
||||
assert.match(phase1, /沒有安裝依賴/);
|
||||
assert.match(phase1, /安裝指令/);
|
||||
assert.match(phase1, /\.env/);
|
||||
});
|
||||
|
||||
test('未處理留言不是 0 時要先停下來提示整併', () => {
|
||||
@@ -50,9 +76,11 @@ test('工作包跨多個 repo 時怎麼開分支,有交代', () => {
|
||||
assert.match(phase1, /有多顆時逐一確認/);
|
||||
});
|
||||
|
||||
test('工作區不乾淨時的處置寫明了,且不替使用者決定', () => {
|
||||
assert.match(phase1, /DIRTY_WORKTREE/);
|
||||
assert.match(phase1, /不要自己選/);
|
||||
test('兩種擋下來的情境各自寫明處置,且都不替使用者決定', () => {
|
||||
assert.match(phase1, /SOURCE_NOT_FOUND/);
|
||||
assert.match(phase1, /不要自己換一個/, '來源分支是使用者的決定,不要自己改指定別支');
|
||||
assert.match(phase1, /WORKTREE_PATH_TAKEN/);
|
||||
assert.match(phase1, /不要自己刪/, '路徑上的東西可能還沒保存,不該由 agent 決定刪掉');
|
||||
});
|
||||
|
||||
test('碼錶只由使用者自己停,並說明為什麼不代勞', () => {
|
||||
@@ -79,13 +107,13 @@ test('--type 什麼時候要給、什麼時候不能給,寫清楚了', () => {
|
||||
});
|
||||
|
||||
test('兩處「不覆蓋他人進度」的保證都有寫出來', () => {
|
||||
assert.match(phase1, /pull 而不是重建/);
|
||||
assert.match(phase1, /起點一律是遠端的來源分支/, '根本不碰本機分支,就沒有覆蓋的可能');
|
||||
assert.match(phase1, /接上去而不是蓋掉/);
|
||||
});
|
||||
|
||||
test('三支腳本的寫入都要求先試跑', () => {
|
||||
test('三支寫入型腳本都要求先試跑', () => {
|
||||
const dryRuns = phase1.match(/--dry-run/g) ?? [];
|
||||
assert.ok(dryRuns.length >= 2, `兩支寫入型腳本各要先試跑,只找到 ${dryRuns.length} 處`);
|
||||
assert.ok(dryRuns.length >= 3, `三支寫入型腳本各要先試跑,只找到 ${dryRuns.length} 處`);
|
||||
});
|
||||
|
||||
test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔', () => {
|
||||
@@ -94,17 +122,24 @@ test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔',
|
||||
assert.match(boundary, /不勾待辦/);
|
||||
assert.match(boundary, /不開 PR/);
|
||||
assert.match(boundary, /不自行建立標籤/);
|
||||
assert.match(boundary, /不在主工作區動手/);
|
||||
assert.match(boundary, /不退回原地切分支/);
|
||||
assert.match(boundary, /不寫任何本機狀態檔/);
|
||||
assert.match(boundary, /換一台機器或換一個 agent/, '要說明為什麼不留狀態檔');
|
||||
});
|
||||
|
||||
// ── 第二段:逐項實作 ───────────────────────────────────────────────
|
||||
|
||||
test('第二段明講改的是工作樹裡的檔案,不是主工作區', () => {
|
||||
assert.match(phase2, /改的是工作樹裡的檔案/);
|
||||
assert.match(phase2, /落到別顆工作包的分支/, '要說明在主工作區動手的後果');
|
||||
});
|
||||
|
||||
test('第二段指名兩份規則正本,且在改檔之前就要讀', () => {
|
||||
assert.match(phase2, /references\/coding-standards\.md/);
|
||||
assert.match(phase2, /references\/comment-styles\.md/);
|
||||
const readAt = phase2.indexOf('coding-standards.md');
|
||||
const implementAt = phase2.indexOf('### 8.');
|
||||
const implementAt = phase2.indexOf('### 9.');
|
||||
assert.ok(readAt < implementAt, '讀規則要排在動手實作之前');
|
||||
});
|
||||
|
||||
@@ -128,7 +163,7 @@ test('資料範例優先取自 MCP,取不到要註明未經驗證', () => {
|
||||
});
|
||||
|
||||
test('回報要點出哪些範例是推理來的', () => {
|
||||
const report = phase2.slice(phase2.indexOf('### 11.'));
|
||||
const report = phase2.slice(phase2.indexOf('### 12.'));
|
||||
assert.match(report, /哪些資料範例是推理來的/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user