正本跟著實作走:分支那一步改成工作樹,新增起錶那一步排在它後面, 並把三處「不要自己決定」寫明——來源分支不存在時不自己換一支、路徑被佔住時 不自己刪、工作樹建不起來時不退回原地切分支。工作區不乾淨的處置整段拿掉: 工作樹本來就是為了讓未提交的變更不再擋路。 AGENTS.md 補上兩條邊界。git worktree add 一定會在目標 repo 的 .git/worktrees/ 底下寫中繼資料,這是 git 的機制,無法避免——「不改目標專案」指的是專案的內容檔, 把這件事明說,免得下一個人以為實作違規。 議題 #40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
5.8 KiB
JavaScript
128 lines
5.8 KiB
JavaScript
/**
|
|
* 正本第一段「領取與開工準備」的規則。
|
|
*
|
|
* 這些檔案是文件不是程式,但它們是指令實際交付的東西:決策表寫錯,被鎖擋下來的人
|
|
* 就會得到錯的下一步;邊界寫漏,第一段就會去做後面幾段的事。靠人記不牢,用測試釘住。
|
|
*/
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
|
|
|
|
const prompt = readPrompt('sdlc-feat');
|
|
/** 第一段的內容,避免把邊界段的字樣誤認成這一段的規則 */
|
|
const phase1 = prompt.slice(prompt.indexOf('## 第一段'), prompt.indexOf('## 邊界'));
|
|
|
|
test('正本平台中立,description 前綴正確', () => {
|
|
assertNeutralPrompt(prompt, 'sdlc-feat');
|
|
});
|
|
|
|
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,
|
|
'領取之前要先讀得懂這顆在做什麼;錶則要等工作樹建好之後才起',
|
|
);
|
|
});
|
|
|
|
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 時要先停下來提示整併', () => {
|
|
assert.match(phase1, /未處理留言數/);
|
|
assert.match(phase1, /先停下來/);
|
|
assert.match(phase1, /sdlc-sync/);
|
|
});
|
|
|
|
test('領取鎖的四種狀態各自交代了下一步,含放行那一種', () => {
|
|
for (const code of ['CLAIMED_BY_OTHER', 'STOPWATCH_ON_THIS_ISSUE', 'STOPWATCH_ON_OTHER_ISSUE']) {
|
|
assert.match(phase1, new RegExp(code), `${code} 要出現在決策表裡`);
|
|
}
|
|
assert.match(phase1, /沒有鎖/, '第四種狀態(放行)也要在表上,否則只剩擋的那幾種');
|
|
assert.match(phase1, /不要繞過去/, '被擋下來的處置要明講,不能靠 agent 自由發揮');
|
|
});
|
|
|
|
test('缺標籤是前置條件,不混進領取鎖的四種狀態裡', () => {
|
|
const table = phase1.slice(phase1.indexOf('| 狀態'), phase1.indexOf('碼錶一律由使用者自己停'));
|
|
assert.equal(table.includes('LABEL_NOT_FOUND'), false, '它不是鎖的狀態,別讓四種變五種');
|
|
assert.match(phase1, /LABEL_NOT_FOUND/, '但仍要交代它,否則使用者不知道怎麼辦');
|
|
});
|
|
|
|
test('工作包跨多個 repo 時怎麼開分支,有交代', () => {
|
|
assert.match(phase1, /repos/);
|
|
assert.match(phase1, /有多顆時逐一確認/);
|
|
});
|
|
|
|
test('兩種擋下來的情境各自寫明處置,且都不替使用者決定', () => {
|
|
assert.match(phase1, /SOURCE_NOT_FOUND/);
|
|
assert.match(phase1, /不要自己換一個/, '來源分支是使用者的決定,不要自己改指定別支');
|
|
assert.match(phase1, /WORKTREE_PATH_TAKEN/);
|
|
assert.match(phase1, /不要自己刪/, '路徑上的東西可能還沒保存,不該由 agent 決定刪掉');
|
|
});
|
|
|
|
test('碼錶只由使用者自己停,並說明為什麼不代勞', () => {
|
|
assert.match(phase1, /由使用者自己停/);
|
|
assert.match(phase1, /工時記錯地方/);
|
|
});
|
|
|
|
test('來源分支要問過使用者,且一次一題、附理由與手動輸入', () => {
|
|
assert.match(phase1, /一次問一題/);
|
|
assert.match(phase1, /手動輸入/);
|
|
assert.match(phase1, /不要替他決定|不要替使用者決定/);
|
|
});
|
|
|
|
test('翻譯規則釘住 kebab 與 40 字元上限,並舉出可照抄的例子', () => {
|
|
assert.match(phase1, /kebab/);
|
|
assert.match(phase1, /40/);
|
|
assert.match(phase1, /wp-extract-contract/, '要有一個真的例子,不要只說規則');
|
|
assert.match(phase1, /不要把長句截斷/);
|
|
});
|
|
|
|
test('--type 什麼時候要給、什麼時候不能給,寫清楚了', () => {
|
|
assert.match(phase1, /`--type` 只在來源是開發分支時要給/);
|
|
assert.match(phase1, /沿用來源/);
|
|
});
|
|
|
|
test('兩處「不覆蓋他人進度」的保證都有寫出來', () => {
|
|
assert.match(phase1, /起點一律是遠端的來源分支/, '根本不碰本機分支,就沒有覆蓋的可能');
|
|
assert.match(phase1, /接上去而不是蓋掉/);
|
|
});
|
|
|
|
test('三支寫入型腳本都要求先試跑', () => {
|
|
const dryRuns = phase1.match(/--dry-run/g) ?? [];
|
|
assert.ok(dryRuns.length >= 3, `三支寫入型腳本各要先試跑,只找到 ${dryRuns.length} 處`);
|
|
});
|
|
|
|
test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔', () => {
|
|
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
|
assert.match(boundary, /不改任何一行程式碼/);
|
|
assert.match(boundary, /不勾待辦/);
|
|
assert.match(boundary, /不開 PR/);
|
|
assert.match(boundary, /不自行建立標籤/);
|
|
assert.match(boundary, /不在主工作區動手/);
|
|
assert.match(boundary, /不退回原地切分支/);
|
|
assert.match(boundary, /不寫任何本機狀態檔/);
|
|
assert.match(boundary, /換一台機器或換一個 agent/, '要說明為什麼不留狀態檔');
|
|
});
|