feat/sdlc-fix-accepts-issue/main #61

Merged
admin merged 4 commits from feat/sdlc-fix-accepts-issue/main into master 2026-09-18 00:52:00 +00:00
4 changed files with 200 additions and 19 deletions
Showing only changes of commit a5f36ed14c - Show all commits
+1 -1
View File
@@ -19,7 +19,7 @@
| `/sdlc-plan` | 把一段口語需求變成結構化的需求議題 |
| `/sdlc-analyze` | 逐題把可行性疑點問到共識,據以產出工作包 |
| `/sdlc-feat` | 領取工作包、開分支、逐項實作並開 PR |
| `/sdlc-fix` | 處理 PR 上的留言 |
| `/sdlc-fix` | 處理 PR 上的留言;收到議題編號則交棒給 `/sdlc-feat` |
| `/sdlc-sync` | 把散落在留言裡的決策整併回議題描述 |
| `/sdlc-report` | 產出週/月/年工時報表 |
+91 -8
View File
@@ -1,17 +1,95 @@
name: sdlc-fix
description: 僅由 /sdlc-fix 指令叫用。定位工作包的工作樹,讀取 PR 上的三類留言,逐條處理並回覆,最後輸出修正摘要。
description: 僅由 /sdlc-fix 指令叫用。收 PR 或議題編號:PR 就定位工作樹、逐條處理三類留言並回覆;議題則交棒給 /sdlc-feat。
# sdlc-fix
reviewer 留完意見,跑這一段,意見被逐條處理並回覆,不漏掉任何一則。
**意見不一定發在 PR 上。** 常常是發在工作包議題或需求議題的留言裡:使用者手上只有一個
議題編號,看得到有人說「這裡要改」。所以輸入收得下三種東西,但只有 PR 在這裡處理完——
議題交棒給 `/sdlc-feat`,不在這裡重做一遍它的流程。
這份檔案是流程正本。各平台的轉接檔只是指回這裡,不要把規則抄過去。
## 輸入
一個 PR 編號。
一個 PR 編號或議題編號。
## 1. 看 PR 現況,並定位工作樹
## 1. 判定輸入是哪一種
三種輸入走三條不同的路,先問清楚是哪一種再動手。**判定沿用既有契約,不另發明判準**——
標籤、標題前綴、編號區間都猜得出來,但猜錯的代價是整條路走錯。
先讀一次留言。它議題與 PR 都收得下,`類型` 欄位就是 PR 與議題的分界:
```
node scripts/pr-comments.js --repo <owner/name> --index <編號>
```
**每個 PR 都是議題,反過來不成立**,所以這個問題只能從議題那一端問:先打 PR 的端點,
遇到純議題會 404,在讀到第一則留言之前就斷了。
`類型` 是 `議題` 時再問一次它是哪一種議題:
```
node scripts/wp-extract.js --repo <owner/name> --index <編號>
```
**`需求議題` 欄位(工作包的母議題)解析得出編號的就是工作包議題,解析不出來的就當需求
議題。** 這是工作包抽取契約本來就有的欄位,`/sdlc-sync` 分這兩種議題用的也是同一份抽取。
| 判定 | 下一步 |
| --- | --- |
| `類型` 為 `PR` | 第 3 步起,這份正本後面每一步都是 PR 的路 |
| `類型` 為 `議題`,抽得出 `需求議題` | 第 2 步的「工作包議題」 |
| `類型` 為 `議題`,抽不出 `需求議題` | 第 2 步的「需求議題」 |
## 2. 議題:交棒給 /sdlc-feat
### 工作包議題
那一顆工作包該怎麼做,`/sdlc-feat`(`prompts/sdlc-feat.md`)已經從領取、備妥工作樹、
逐項實作一路定到開出 PR。**照那份正本走,把這個編號當成它的輸入。**
**不要把它的步驟搬過來重講一遍**:那會製造第二份正本,兩邊遲早分岔——而使用者不會知道
自己讀到的是哪一份。
交棒沿用 `/sdlc-sync` 那一套接回機制,只是方向相反:**直接接下去做,不要求使用者重打
指令**。他已經給過這個編號了。
留言的整併不必在這裡先做——`/sdlc-feat` 的第一步就會看 `未處理留言數`,該整併時它自己
會轉去 `/sdlc-sync`。在這裡先做一次,等於把那一步也抄了過來。
### 需求議題
需求議題上的留言**絕大多數是決策討論**,不是「這一行要改」。所以先整併,再談改碼:
1. 第 1 步的 `未處理數` 大於 0 就**走 `/sdlc-sync` 的流程**(`prompts/sdlc-sync.md`),
做完**自動接回這裡**——重新讀一次留言,拿到的才是剛整併過的狀態。同樣不要求使用者重打指令。
2. 整併(與使用者選擇略過)之後還剩下的留言裡,挑出**確實要求改程式碼**的那幾則。判斷
依據與第 5 步的「必改/建議」同一套:指出了錯誤、遺漏、會出事的寫法,或明確要求改動。
3. **一則都沒有就到此為止。** 把整併了幾則、略過幾則講清楚,說明沒有要改碼的意見,
然後停下來。先 sync 一次通常就清空了,選工作包那一步根本不會觸發。
確實有要改的,就列出這顆需求底下的工作包,讓使用者挑一顆:
```
node scripts/wp-list.js --repo <owner/name> --requirement <需求議題編號>
```
**不要因為「輸入不是工作包」就報錯。** 挑哪一顆他無論如何都要挑,報錯只是把這件事推回去
讓他自己在 Gitea 網頁上翻。
一次問一題,選項是清單上的工作包(帶編號、標題、狀態、領取人),外加**手動輸入**——
清單以「關聯」段落認歸屬,漏掉的那一顆他自己給得出編號。附上你的判斷:哪一顆的範圍
涵蓋得到那幾則留言說的地方,以及為什麼。
清單是空的就照實說:這顆需求底下還沒有工作包,該跑的是 `/sdlc-analyze`,不是這一支。
挑定之後**交棒給 `/sdlc-feat`**,與上一小節同一條路。**把那幾則要求改碼的留言一起帶過去**,
它們是這一輪要做的事;留言本身留在需求議題上不動,交棒不搬走任何人說過的話。
## 3. 看 PR 現況,並定位工作樹
先問一次現況,再決定要不要動手:
@@ -56,12 +134,14 @@ node scripts/worktree-ensure.js --repo <owner/name> --path <目標專案路徑>
**後面每一步都在那棵工作樹裡做**,不要回到主工作區:它可能停在別的分支上,在那裡改
會把改動落到別顆工作包的分支去。
## 2. 讀留言
## 4. 讀留言
```
node scripts/pr-comments.js --repo <owner/name> --index <PR 編號>
```
第 1 步判定型別時讀的就是這一份,**手上那一份還在就直接用,不必再讀一次**。
三類留言一次讀齊:**一般留言**、**review 總評**、**行內留言**。每一則都帶 `id`、`作者`、
`內容`、`已處理`,行內的還帶 `檔案`/`行`/`diff`——那段 diff 是判斷「他在說哪裡」的依據,
不要略過不看。
@@ -76,7 +156,7 @@ resolve。`已處理` 認的是**自己打的** `+1`——reviewer 對留言按
偶爾會遇到 `可標記` 是 `false` 的總評(在 timeline 上對不到它在 issue comment 表裡的
那一份)。那一則回覆照發,但沒有記號留得下來,**要在修正摘要裡單獨點出來**。
## 3. 分類:必改還是建議
## 5. 分類:必改還是建議
逐則判斷,**reviewer 不必逐則說明**。判斷依據是內容本身:
@@ -87,7 +167,7 @@ resolve。`已處理` 認的是**自己打的** `+1`——reviewer 對留言按
分類錯的代價不對稱——把必改當成建議會漏掉真的問題,所以拿不準時歸到必改那一邊,
並在下一步問清楚。
## 4. 不確定就問
## 6. 不確定就問
**一次問一題。** 下列情況不要自作主張:
@@ -102,7 +182,7 @@ resolve。`已處理` 認的是**自己打的** `+1`——reviewer 對留言按
**不確定卻硬改,比多問一題貴得多。** 改壞的地方 reviewer 下一輪才會看到。
## 5. 逐則處理並回覆
## 7. 逐則處理並回覆
一則一則來:先改,改完立刻回覆那一則,再處理下一則。**不要全部改完才一起回**——
中途斷掉的話,沒有人知道哪幾則已經處理過。
@@ -126,7 +206,7 @@ node scripts/pr-reply.js --repo <owner/name> --index <PR 編號> \
不必自己去翻 diff。決定不改的也要回,並說明理由——建議類的留言常常合理地不採納,
但沉默會讓 reviewer 以為被忽略了。
## 6. 修正摘要
## 8. 修正摘要
全部處理完後印一則摘要,讓 reviewer 不必逐串點開:
@@ -149,3 +229,6 @@ node scripts/pr-reply.js --repo <owner/name> --index <PR 編號> \
- 不動 PR 的 review 狀態:回覆一則意見不該順手把整個 PR 標成通過或要求變更。
- **不在主工作區處理留言**,一律在 `worktree-ensure` 定位出來的那棵工作樹裡。
- PR 已經合併或關閉時不繼續處理留言,也不自己去刪推導路徑上的東西。
- **不重做 `/sdlc-feat` 的步驟,也不把它的步驟抄進這份正本**:議題一律交棒過去。
- 不因為輸入是議題就報錯要使用者改打別的指令,也不要求他把交棒過的指令重打一次。
- 不替使用者決定要在哪一顆工作包上改:列出清單,讓他挑。
+2 -2
View File
@@ -89,8 +89,8 @@ node scripts/comments-merge.js --repo <owner/name> --index <編號> \
## 接回原本的指令
這個指令常常不是使用者自己叫的,而是 `/sdlc-analyze` 或 `/sdlc-feat` 發現有未整併留言後
轉過來的。**整併完就直接接回去**,從原本那個指令被打斷的地方繼續,不要要求使用者重打一次。
這個指令常常不是使用者自己叫的,而是 `/sdlc-analyze`、`/sdlc-feat` 或 `/sdlc-fix` 發現有
未整併留言後轉過來的。**整併完就直接接回去**,從原本那個指令被打斷的地方繼續,不要要求使用者重打一次。
接回去之前先重跑一次抽取(`issue-extract`/`wp-extract`),拿到的才是剛更新過的描述——
接著用舊的那一份做事,這一整段就白做了。
+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, /以為它們被跳過/, '要說出為什麼得單獨列');