From c56f3271b5180b59ba663ebfd1bd412def6f8eff Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 22 Sep 2026 15:31:36 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(workflow-assets):=20=E6=95=B4=E5=90=88?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=AD=A3=E6=9C=AC=E8=88=87=E5=A7=94=E6=B4=BE?= =?UTF-8?q?=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以能力描述補齊可委派步驟,讓流程正本、委派規則、AGENTS、README 與 ADR 保持同一份繁體中文與 UTF-8 契約。 --- prompts/sdlc-analyze.md | 11 ++++++++++- prompts/sdlc-feat.md | 6 ++++++ prompts/sdlc-plan.md | 24 +++++++++++++++++++----- references/delegation.md | 10 ++++------ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/prompts/sdlc-analyze.md b/prompts/sdlc-analyze.md index eba8b97..ef7dadc 100644 --- a/prompts/sdlc-analyze.md +++ b/prompts/sdlc-analyze.md @@ -9,6 +9,8 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可 ## 第一段:可行性分析 +### 1. 對四份清單列出疑點〔可委派〕 + 依序讀取並逐條對照: 1. `references/feasibility-architecture.md` @@ -16,6 +18,10 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可 3. `references/feasibility-data.md` 4. `references/feasibility-schedule.md` +只產出可核對的疑點清單,不替使用者做決策;你的環境若能把工作交給子代理,就交出去,只把清單帶回來;不能就自己做。 + +### 2. 逐題確認可行性共識 + 能從程式碼查證的事項自行查證;只有需要使用者決策的事項才提問。架構、邏輯、資料、時程四類依序完成,每次只問一題,每題提供建議與理由,以及手動輸入的方式。最後輸出共識摘要、變更假設、未決事項與人天估算;摘要只印終端,不寫入 Gitea。 ### 交付文件判斷 @@ -49,7 +55,10 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可 ## 第三段:排程 -所有工作包建立且相依關係確認後,以 `startDate`、`days` 與 `depends` 組成計畫檔,執行 `scripts/schedule.js` 計算截止日;依序用 `issue-link.js`、`issue-update.js` 與 `project-add.js` 補上既有相依、Milestone、看板、截止日與人天估算。各腳本先 dry-run,再實跑。日期與人天是排程資料,不是耗時統計。 +### 3. 算出截止日〔可委派〕 + +所有工作包建立且相依關係確認後,只依已確認的 `startDate`、`days` 與 `depends` 呼叫 `scripts/schedule.js` 計算截止日;這一步只回傳可核對的日期與相依結果,你的環境若能把工作交給子代理,就交出去,只把結果帶回來;不能就自己做。寫入議題、Milestone、看板與其他後續資料不委派。 +計算完成後,依序用 `issue-link.js`、`issue-update.js` 與 `project-add.js` 補上既有相依、Milestone、看板、截止日與人天估算。各腳本先 dry-run,再實跑。日期與人天是排程資料,不是耗時統計。 ## 邊界 diff --git a/prompts/sdlc-feat.md b/prompts/sdlc-feat.md index 911d7ac..6b9f401 100644 --- a/prompts/sdlc-feat.md +++ b/prompts/sdlc-feat.md @@ -24,8 +24,14 @@ API 契約文件只能交付在預覽位置或使用者確認的位置;不得 ### ELI5 變體 使用者要求 ELI5 時,仍保留原文件的範圍、順序、相依、例外與驗收意義;把術語換成日常說法並補必要的短解釋,不刪除技術限制。圖表要重新繪製成容易閱讀的圖片式視覺,不把 Mermaid 原碼當成交付物,也不只用一個看似精確的日期隱藏 O/M/P 不確定性。 +### 把議題標題翻成英文〔可委派〕 + +把工作包議題標題轉成不超過 40 字元的英文 kebab slug,保留原意且不捏造新範圍;這一步只產出可驗證的 slug,你的環境若能把工作交給子代理,就交出去,只把 slug 帶回來;不能就自己做。若有兩個同樣合理的翻法,交回候選與差異,由主流程詢問使用者。 ## 實作與交付 +### 分批提交方案〔可委派〕 + +依檔案類型與變更性質計算 commit 分類、順序與每批檔案;只回傳可核對的提交方案。實際執行 `scripts/commit-split.js`、處理失敗與確認 git 歷史不委派,由主流程自己完成。 完成程式碼待辦後照既有測試與驗證慣例;文件待辦則依上述分流交付。完成後依既有 commit 分類規則提交,先用 `scripts/pr-create.js --dry-run` 檢查,再實跑開 PR。回報工作包、分支、worktree、完成待辦、commit 與 PR。 diff --git a/prompts/sdlc-plan.md b/prompts/sdlc-plan.md index 7385ba2..c9387db 100644 --- a/prompts/sdlc-plan.md +++ b/prompts/sdlc-plan.md @@ -7,11 +7,25 @@ description: 僅由 /sdlc-plan 指令叫用。把一段口語需求轉成結構 ## 步驟 -1. 讀齊輸入,列出九個段落中已有依據與缺漏。 -2. 一次問一題補齊缺漏;未獲回答的內容放入「未決事項」,不得自行編造。 -3. 套用 `templates/requirement-issue.md`,填入總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項;全程使用繁體中文。 -4. 用 `scripts/labels-list.js` 取得既有標籤,只能選既有標籤。 -5. 寫入前先執行: +### 1. 列出九段落依據與缺漏〔可委派〕 + +讀齊輸入,列出總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項中已有依據與缺漏。這一步只產出可核對的清單;你的環境若能把工作交給子代理,就交出去,只把清單帶回來;不能就自己做。 + +### 2. 一次問一題補齊缺漏 + +一次問一題補齊缺漏;未獲回答的內容放入「未決事項」,不得自行編造。 + +### 3. 填入需求議題模板 + +套用 `templates/requirement-issue.md`,填入總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項;全程使用繁體中文。 + +### 4. 取得既有標籤 + +用 `scripts/labels-list.js` 取得既有標籤,只能選既有標籤。 + +### 5. 試跑並建立議題 + +寫入前先執行: ``` node scripts/issue-create.js --repo --title "<標題>" --body-file <暫存檔> --labels "<標籤>" --dry-run diff --git a/references/delegation.md b/references/delegation.md index ec9fca3..e6b71de 100644 --- a/references/delegation.md +++ b/references/delegation.md @@ -36,8 +36,7 @@ 一個步驟裡只有一半合判準時,**標記照下,並在該步寫明哪一半不委派**。這比整步不標好—— 不標的話那一半的中間產物照樣塞滿主脈絡;也比整步委派安全,因為第四條是硬排除。 -目前有三步是這個形狀:兩份圖解總覽(產出 HTML 可委派,寫回議題的 `issue-update` 不委派) -與分批提交(方案計算可委派,實際跑 `commit-split.js` 不委派)。 +目前有一步是這個形狀:分批提交的方案計算可委派,實際跑 `commit-split.js` 不委派。 ## 目前標記為〔可委派〕的步驟 @@ -46,12 +45,11 @@ | 正本 | 步驟 | 委派範圍 | | --- | --- | --- | -| `sdlc-plan` | 產生圖解版總覽 | 產出 HTML;寫回議題不委派 | +| `sdlc-plan` | 列出九段落依據與缺漏 | 產出可核對的清單 | | `sdlc-analyze` | 對四份清單列出疑點 | 全步 | -| `sdlc-analyze` | 算出截止日 | 全步 | -| `sdlc-analyze` | 產生分析版的圖解總覽 | 產出 HTML;寫回議題不委派 | +| `sdlc-analyze` | 算出截止日 | 計算日期;寫回議題不委派 | | `sdlc-feat` | 把議題標題翻成英文 | 全步 | -| `sdlc-feat` | 分批提交 | 方案計算;實際提交不委派 | +| `sdlc-feat` | 分批提交方案 | 方案計算;實際提交不委派 | `sdlc-sync`、`sdlc-fix` 與 `sdlc-report` 目前沒有可委派的步驟:前兩者每一步都在問使用者 或寫入 Gitea,後者只有一支唯讀腳本,委派出去省不到什麼。 From 43e91ceb702363d36d790d6a5c9e37c8b88f954d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 22 Sep 2026 15:31:36 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test(workflow-assets):=20=E6=95=B4=E5=90=88?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=AD=A3=E6=9C=AC=E8=88=87=E5=A7=94=E6=B4=BE?= =?UTF-8?q?=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以能力描述補齊可委派步驟,讓流程正本、委派規則、AGENTS、README 與 ADR 保持同一份繁體中文與 UTF-8 契約。 --- test/bin-entry.test.js | 3 +- test/delegation-assets.test.js | 63 ++++++++++++++++++++++++++++++++++ test/encoding-assets.test.js | 30 ++++++++++++++++ test/install-verify.test.js | 6 ++-- test/install.test.js | 21 ++++++++++++ 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 test/delegation-assets.test.js create mode 100644 test/encoding-assets.test.js diff --git a/test/bin-entry.test.js b/test/bin-entry.test.js index 54fb8fd..7926bba 100644 --- a/test/bin-entry.test.js +++ b/test/bin-entry.test.js @@ -81,7 +81,8 @@ test('打包內容以白名單決定:四個正本目錄都在,測試與暫 const packed = JSON.parse( execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: repoRoot, encoding: 'utf8' }), ); - const files = packed[0].files.map((file) => file.path); + const manifest = Array.isArray(packed) ? packed[0] : packed[Object.keys(packed)[0]]; + const files = manifest.files.map((file) => file.path); for (const dir of ['prompts/', 'scripts/', 'templates/', 'references/', 'bin/']) { assert.ok( diff --git a/test/delegation-assets.test.js b/test/delegation-assets.test.js new file mode 100644 index 0000000..58cd24a --- /dev/null +++ b/test/delegation-assets.test.js @@ -0,0 +1,63 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readReference, readPrompt, assertNeutralPrompt } from './helpers/prompt-doc.js'; + +const RULES = readReference('delegation'); +const PROMPTS = ['sdlc-plan', 'sdlc-analyze', 'sdlc-feat', 'sdlc-fix', 'sdlc-sync', 'sdlc-report']; +const EXPECTED = [ + ['sdlc-plan', '列出九段落依據與缺漏'], + ['sdlc-analyze', '對四份清單列出疑點'], + ['sdlc-analyze', '算出截止日'], + ['sdlc-feat', '把議題標題翻成英文'], + ['sdlc-feat', '分批提交方案'], +]; + +function tableEntries() { + return [...RULES.matchAll(/^\| `([^`]+)` \| ([^|]+) \|/gm)] + .map((match) => [match[1], match[2].trim()]); +} + +function promptMarkers(name) { + const prompt = readPrompt(name); + return [...prompt.matchAll(/^### (?:\d+\.\s+)?(.+?)〔可委派〕\s*$/gm)] + .map((match) => [name, match[1].trim()]); +} + +// ── 四條判準與雙向集合 ──────────────────────────────────────────── + +test('委派規則保留四條判準與兩條硬排除', () => { + for (const term of ['可驗證的成品', '不會詢問使用者', '失敗能被呼叫端偵測', '不直接寫入 Gitea 或 git']) { + assert.match(RULES, new RegExp(term)); + } +}); + +test('委派表與六份正本的標記集合完全一致', () => { + const table = tableEntries(); + const markers = PROMPTS.flatMap(promptMarkers); + assert.deepEqual(table, EXPECTED); + assert.deepEqual(markers, EXPECTED); +}); + +test('每個標記步驟都說明能力降級與部分委派邊界', () => { + for (const [name, step] of EXPECTED) { + const prompt = readPrompt(name); + const marker = prompt.match(new RegExp(`^### (?:\\d+\\.\\s+)?${step}〔可委派〕\\s*$`, 'm')); + assert.ok(marker, `${name} 缺少標記:${step}`); + const start = marker.index; + const body = prompt.slice(start, prompt.indexOf('\n### ', start + 1) === -1 ? prompt.length : prompt.indexOf('\n### ', start + 1)); + assert.match(body, /你的環境若能把工作交給子代理|實際執行.*不委派/); + } +}); + +// ── 六份正本的平台與文字契約 ────────────────────────────────────── + +test('六份流程正本均為平台中立且使用正確 description 前綴', () => { + for (const name of PROMPTS) assertNeutralPrompt(readPrompt(name), name); +}); + +test('沒有委派標記的流程仍明確列入規則表的空集合', () => { + for (const name of ['sdlc-fix', 'sdlc-sync', 'sdlc-report']) { + assert.equal(promptMarkers(name).length, 0, `${name} 不應偷偷出現委派步驟`); + } + assert.match(RULES, /sdlc-sync.*sdlc-fix.*sdlc-report/); +}); diff --git a/test/encoding-assets.test.js b/test/encoding-assets.test.js new file mode 100644 index 0000000..ecc7299 --- /dev/null +++ b/test/encoding-assets.test.js @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { repoRoot } from './helpers/run-script.js'; + +const files = [ + ...['sdlc-plan', 'sdlc-analyze', 'sdlc-feat', 'sdlc-fix', 'sdlc-sync', 'sdlc-report'] + .map((name) => join(repoRoot, 'prompts', `${name}.md`)), + join(repoRoot, 'references', 'delegation.md'), + join(repoRoot, 'AGENTS.md'), + join(repoRoot, 'README.md'), + join(repoRoot, 'docs', 'adr', '0001-以集中式雜湊路徑的-worktree-隔離平行工作包.md'), + join(repoRoot, 'docs', 'adr', '0002-以能力描述而非工具名表達委派.md'), +]; + +test('流程、規則與文件資產都是合法 UTF-8 且沒有替代字元', () => { + for (const path of files) { + const bytes = readFileSync(path); + const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + assert.equal(text.includes('\uFFFD'), false, `${path} 含替代字元`); + } +}); + +test('六份流程正本都以繁體中文 description 開頭', () => { + for (const path of files.slice(0, 6)) { + const text = readFileSync(path, 'utf8'); + assert.match(text, /^description: 僅由 \/sdlc-[a-z-]+ 指令叫用。/m, path); + } +}); diff --git a/test/install-verify.test.js b/test/install-verify.test.js index d9dd70d..0e57110 100644 --- a/test/install-verify.test.js +++ b/test/install-verify.test.js @@ -9,7 +9,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { manifest, tmpRoot } from './helpers/run-script.js'; +import { manifest, pathWithOnly, tmpRoot } from './helpers/run-script.js'; import { fakePrompt, makeFakePlugin } from './helpers/fake-plugin.js'; import { startStubGitea } from './helpers/stub-gitea.js'; @@ -114,7 +114,7 @@ test('PATH 上找不到 tea-sdlc 時整體 ok:false,並指出病灶在 PATH const plugin = makeFakePlugin(t, { prompts: PROMPTS }); const home = makeHome(t); - const { code, json } = await inHome(plugin, home)(['install'], { shim: false }); + const { code, json } = await inHome(plugin, home)(['install'], { shim: false, path: pathWithOnly(['node']) }); assert.equal(code, 1); assert.equal(json.ok, false); @@ -261,7 +261,7 @@ test('驗證失敗時已經寫好的轉接檔一份都不刪', async (t) => { const home = makeHome(t); // 病灶在 PATH,不在轉接檔:刪掉轉接檔只會讓使用者從「有點舊但能用」變成什麼都沒有 - const { json } = await inHome(plugin, home)(['install'], { shim: false }); + const { json } = await inHome(plugin, home)(['install'], { shim: false, path: pathWithOnly(['node']) }); assert.equal(json.ok, false); for (const path of adaptersOf(json)) { diff --git a/test/install.test.js b/test/install.test.js index 9d9079f..1eef008 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -176,6 +176,27 @@ test('支援 command 的四個平台產生 command 轉接檔,另外三個產 assert.deepEqual(filesUnder(join(home, 'work', '.github')), ['skills/sdlc-plan/SKILL.md']); }); +test('七個平台的轉接檔都保留繁體中文且沒有 UTF-8 亂碼', async (t) => { + const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') } }); + const home = makeHome(t, ['claude', 'codex', 'opencode', 'oh-my-pi', 'antigravity', 'kiro', 'copilot']); + + await inHome(plugin, home)(['install']); + const files = [ + join(home, '.claude', 'commands', 'sdlc-plan.md'), + join(home, '.codex', 'prompts', 'sdlc-plan.md'), + join(home, '.config', 'opencode', 'command', 'sdlc-plan.md'), + join(home, '.omp', 'agent', 'commands', 'sdlc-plan.md'), + join(home, '.gemini', 'skills', 'sdlc-plan', 'SKILL.md'), + join(home, '.kiro', 'skills', 'sdlc-plan', 'SKILL.md'), + join(home, 'work', '.github', 'skills', 'sdlc-plan', 'SKILL.md'), + ]; + + for (const path of files) { + const text = new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(path)); + assert.match(text, /僅由 \/sdlc-plan 指令叫用。/); + } +}); + test('轉接檔內容是一句指向 tea-sdlc 的話,不含任何檔案路徑', async (t) => { const plugin = makeFakePlugin(t, { prompts: PROMPTS, version: '1.2.3' }); const home = makeHome(t, ['claude']); From 6080cc99dab498984073174d5f9d003714a29cac Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 22 Sep 2026 15:31:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs(workflow-assets):=20=E6=95=B4=E5=90=88?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=AD=A3=E6=9C=AC=E8=88=87=E5=A7=94=E6=B4=BE?= =?UTF-8?q?=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以能力描述補齊可委派步驟,讓流程正本、委派規則、AGENTS、README 與 ADR 保持同一份繁體中文與 UTF-8 契約。 --- AGENTS.md | 2 ++ README.md | 7 +++++-- docs/adr/0002-以能力描述而非工具名表達委派.md | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c0569c0..ebba66c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,8 @@ ## 慣例 - **零外部套件**:`package.json` 不得出現 `dependencies` 或 `devDependencies`。測試用 Node 內建 `node:test` + `node:assert`。 +- **委派標記雙向一致**:流程正本的 `〔可委派〕` 集合必須與 `references/delegation.md` 相同;任何變更同步更新資產測試與 ADR。 +- **文字編碼**:流程正本、規則正本、README、AGENTS.md 與 ADR 一律以 UTF-8 儲存,面向使用者的文字維持繁體中文。 - **契約以議題為正本**:腳本的 flag 介面、JSON 輸出形狀、前置檢查與路徑定位規則,正本在[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),實作時以該處為準;本檔不複寫,以免兩邊走鐘。 - **測試**:`npm test`(等同 `node --test`)。測試產生的暫存一律寫到 `.tmp/`,該目錄已被 git 忽略,也不會被測試探索掃到。 - **不改目標專案**:本 plugin 只讀目標專案的程式碼,不寫入目標專案的 `CLAUDE.md` 或任何設定檔。 diff --git a/README.md b/README.md index 76894d2..91f8fcc 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ - **副作用集中**:所有對 Gitea 與 git 的呼叫下沉到 `scripts/` 的零相依 Node 腳本,統一 JSON 輸入輸出。 - **產出有固定形狀**:議題、PR、報表一律套 `templates/` 的模板。 -> 六個流程正本都到齊了,`tea-sdlc install` 會把它們一次佈署到偵測到的平台。 -> 完整需求見[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),進度見其底下的工作包。 +> 六個流程正本都到齊了;`tea-sdlc install` 會把它們一次佈署到偵測到的平台。委派標記以 +> `references/delegation.md` 為對照正本,資產測試會檢查雙向一致;流程與文件均以 UTF-8 +> 儲存並維持繁體中文。進度見 +> [議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1) 底下的工作包。 --- @@ -41,6 +43,7 @@ tea-sdlc/ ├── plugin.json # Antigravity 的 plugin manifest ├── package.json # npm 打包與測試入口,無任何相依套件 ├── AGENTS.md # 給 AI 助理的模組邊界與慣例 +├── docs/adr/ # 已接受的架構決策紀錄 └── README.md ``` diff --git a/docs/adr/0002-以能力描述而非工具名表達委派.md b/docs/adr/0002-以能力描述而非工具名表達委派.md index 6b582a1..2230e2d 100644 --- a/docs/adr/0002-以能力描述而非工具名表達委派.md +++ b/docs/adr/0002-以能力描述而非工具名表達委派.md @@ -5,6 +5,7 @@ status: accepted # 以能力描述而非工具名表達委派 流程正本在只在意結果的步驟上標記 `〔可委派〕`,並以**能力描述**說明怎麼委派——「你的環境若能把工作交給子代理,就交出去,只把結果帶回來;不能就自己做」——而不指名任何平台的子代理工具。子代理是平台專屬能力(Claude Code 與 Codex 有,Copilot/Kiro/OpenCode 不一定),而流程正本必須保持平台中立:這是 #4 已交付並打勾的驗收標準,也是 `AGENTS.md` 的模組邊界之一。能力描述對不支援的平台是自然降級,同一份正本兩邊都讀得通,不需要維護兩份。 +目前的標記集合是:`sdlc-plan` 列出九段落依據與缺漏、`sdlc-analyze` 對四份清單列出疑點與算出截止日,以及 `sdlc-feat` 把議題標題翻成英文與計算分批提交方案。分批提交的實際執行仍由主流程處理;完整對照以 `references/delegation.md` 為準。 ## Considered Options