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:
2026-09-17 15:54:27 +08:00
co-authored by Claude Opus 5
parent feaf40ab78
commit ba714b0a00
3 changed files with 97 additions and 24 deletions
+40 -10
View File
@@ -16,11 +16,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 時要先停下來提示整併', () => {
@@ -48,9 +74,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('碼錶只由使用者自己停,並說明為什麼不代勞', () => {
@@ -77,13 +105,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('邊界把第一段不做的事分開列,且明講不寫本機狀態檔', () => {
@@ -92,6 +120,8 @@ test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔',
assert.match(boundary, /不勾待辦/);
assert.match(boundary, /不開 PR/);
assert.match(boundary, /不自行建立標籤/);
assert.match(boundary, /不在主工作區動手/);
assert.match(boundary, /不退回原地切分支/);
assert.match(boundary, /不寫任何本機狀態檔/);
assert.match(boundary, /換一台機器或換一個 agent/, '要說明為什麼不留狀態檔');
});