feat(sdlc-fix): 輸入放寬為 PR 編號或議題編號

議題不自己重做一遍實作流程,而是交棒給 /sdlc-feat:工作包議題直接交棒,
需求議題先讓 /sdlc-sync 整併決策留言,剩下確實要改碼的才列出底下的工作包
讓使用者挑一顆。交棒複用 sdlc-sync 已經定好的接回機制,使用者不必重打指令。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 18:16:23 +08:00
co-authored by Claude Opus 5
parent 09d880e6f2
commit a5f36ed14c
4 changed files with 200 additions and 19 deletions
+106 -8
View File
@@ -1,9 +1,9 @@
/**
* /sdlc-fix 的流程正本。
*
* 這一段有三件事只有正本做得到,腳本擋不住:分類必改/建議、不確定時停下來問、
* 以及最後那則摘要。寫漏任何一件,reviewer 的意見就會被靜靜跳過——而那正是這個指令
* 存在的理由。
* 這一段有四件事只有正本做得到,腳本擋不住:把三種輸入判到對的路上、分類必改/建議、
* 不確定時停下來問、以及最後那則摘要。寫漏任何一件,reviewer 的意見就會被靜靜跳過——
* 而那正是這個指令存在的理由。
*/
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -11,10 +11,108 @@ import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
const prompt = readPrompt('sdlc-fix');
const steps = prompt.slice(prompt.indexOf('## 1.'), prompt.indexOf('## 邊界'));
/** 定位那一步,與後面處理留言的步驟分開看 */
const locate = steps.slice(0, steps.indexOf('## 2.'));
test('第一步先看 PR 現況,終止狀態就停下來不白做工', () => {
/** 框出一個步驟的範圍,讓各步驟分開看 */
const step = (from, to) => steps.slice(steps.indexOf(from), steps.indexOf(to));
/** 判定輸入是哪一種 */
const 判定 = step('## 1.', '## 2.');
/** 議題交棒那一步 */
const 交棒 = step('## 2.', '## 3.');
/** 工作包議題與需求議題各自的小節 */
const 工作包路 = 交棒.slice(交棒.indexOf('### 工作包議題'), 交棒.indexOf('### 需求議題'));
const 需求路 = 交棒.slice(交棒.indexOf('### 需求議題'));
/** 定位工作樹那一步,與後面處理留言的步驟分開看 */
const locate = step('## 3.', '## 4.');
// ── 輸入判定:三種輸入走三條路 ─────────────────────────────────────
test('輸入接受 PR 編號或議題編號', () => {
const 輸入 = prompt.slice(prompt.indexOf('## 輸入'), prompt.indexOf('## 1.'));
assert.match(輸入, /PR 編號或議題編號/);
});
test('PR 與議題的分界用既有的留言讀取契約,不另發明判準', () => {
assert.match(判定, /pr-comments\.js/);
assert.match(判定, /類型/);
assert.match(判定, /每個 PR 都是議題/, '要說出為什麼只能從議題那一端問');
assert.match(判定, /不另發明判準/);
});
test('兩種議題的分界是工作包抽取的母議題欄位', () => {
assert.match(判定, /wp-extract\.js/);
assert.match(判定, /需求議題.*母議題|母議題/);
assert.match(判定, /解析得出編號的就是工作包議題/);
assert.match(判定, /解析不出來的就當需求\s*議題/);
});
test('三種判定各自寫明下一步,不留一種讓 agent 自由發揮', () => {
const rows = 判定.slice(判定.indexOf('| 判定'));
assert.match(rows, /`類型` 為 `PR`/);
assert.match(rows, /抽得出 `需求議題`/);
assert.match(rows, /抽不出 `需求議題`/);
});
// ── 議題:交棒給 /sdlc-feat ────────────────────────────────────────
test('工作包議題交棒給 /sdlc-feat,指的是它的正本而不是複述它', () => {
assert.match(工作包路, /sdlc-feat/);
assert.match(工作包路, /prompts\/sdlc-feat\.md/);
assert.match(工作包路, /不要把它的步驟搬過來重講一遍/);
assert.match(工作包路, /第二份正本/, '要說出為什麼不抄');
});
test('交棒不要求使用者重打指令,並指名沿用既有的接回機制', () => {
assert.match(交棒, /sdlc-sync/);
assert.match(交棒, /接回|接下去/);
assert.match(交棒, /不要求使用者重打\s*指令/);
});
test('工作包議題不在這裡先整併留言:那一步 /sdlc-feat 自己會做', () => {
assert.match(工作包路, /未處理留言數/);
assert.match(工作包路, /不必在這裡先做/);
});
test('正本裡不出現 /sdlc-feat 步驟的複製', () => {
for (const 腳本 of ['claim.js', 'branch-prep.js', 'timer.js', 'commit-split.js', 'pr-create.js']) {
assert.equal(prompt.includes(腳本), false, `這是 /sdlc-feat 的步驟,不該被抄進來:${腳本}`);
}
});
test('需求議題先讓 /sdlc-sync 整併,再談改不改碼', () => {
assert.match(需求路, /sdlc-sync/);
assert.match(需求路, /prompts\/sdlc-sync\.md/);
assert.match(需求路, /絕大多數是決策討論/, '要說出為什麼先 sync');
assert.ok(
需求路.indexOf('sdlc-sync') < 需求路.indexOf('wp-list.js'),
'整併要排在選工作包之前',
);
});
test('整併完沒有改碼要求就停下來,不硬找一顆工作包來改', () => {
assert.match(需求路, /一則都沒有就到此為止/);
assert.match(需求路, /根本不會觸發/);
});
test('有改碼要求時列出底下的工作包讓使用者挑,不報錯把事推回去', () => {
assert.match(需求路, /wp-list\.js/);
assert.match(需求路, /--requirement/);
assert.match(需求路, /不要因為「輸入不是工作包」就報錯/);
assert.match(需求路, /一次問一題/);
assert.match(需求路, /手動輸入/);
});
test('清單為空時照實說,並指出該跑的是哪一支', () => {
assert.match(需求路, /清單是空的/);
assert.match(需求路, /sdlc-analyze/);
});
test('挑定之後一樣交棒給 /sdlc-feat,留言只帶脈絡不搬走', () => {
assert.match(需求路, /交棒給 `\/sdlc-feat`/);
assert.match(需求路, /不搬走/);
});
test('PR 的第一步先看現況,終止狀態就停下來不白做工', () => {
assert.match(locate, /pr-watch\.js/);
assert.match(locate, /terminal/);
assert.match(locate, /不要繼續處理留言/);
@@ -118,7 +216,7 @@ test('一次問一題,選項含手動輸入', () => {
});
test('該問的情況有列舉,不是一句「不確定就問」', () => {
const section = steps.slice(steps.indexOf('## 4.'), steps.indexOf('## 5.'));
const section = steps.slice(steps.indexOf('## 6.'), steps.indexOf('## 7.'));
const bullets = section.match(/^- /gm) ?? [];
assert.ok(bullets.length >= 3, `該問的情況要列得出來,只找到 ${bullets.length} 條`);
assert.match(section, /推了新 commit/, '位置對不上是最常見的一種,要點名');
@@ -159,7 +257,7 @@ test('寫入前要求先試跑', () => {
// ── 摘要 ───────────────────────────────────────────────────────────
test('摘要要逐則列出,並單獨點出沒有記號的那幾則', () => {
const section = steps.slice(steps.indexOf('## 6.'));
const section = steps.slice(steps.indexOf('## 8.'));
assert.match(section, /逐則一行/);
assert.match(section, /沒有留下記號的那幾則/);
assert.match(section, /以為它們被跳過/, '要說出為什麼得單獨列');