Author SHA1 Message Date
admin b6aa8e09a2 Merge pull request '修正 wp-gate.sh check-deps 的 awk 欄位切析錯誤,讓相依關卡能正確攔下未合併相依的工作包' (#62) from fix/wp-gate-check-deps-field-index into develop
Reviewed-on: #62
2026-09-08 02:12:26 +00:00
jiantw83andClaude Sonnet 5 59c198fb06 fix(sdlc): 修正 wp-gate check-deps 相依欄位解析錯位問題
原本 awk -F'|' 固定用第 6 欄當相依欄,但表格每列開頭多一個空欄,實際相依欄是第 7 欄,
導致每個候選工作包都被誤判成無相依可直接挑選,相依關卡形同虛設。同時 PR/狀態欄改用
從結尾往回數,避免層 0 多一欄「工作證」時與層 1 到 4 的表格欄位對不齊;並在切欄前先
處理儲存格內跳脫的字面 |,避免該列後續欄位整批錯位。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:07:33 +08:00
admin 24f4d5e4a7 Merge pull request '五個階段目錄頁範本改成大標題加條列,四支技能的讀寫敘述同步' (#60) from feat/contents-list/main into develop
Reviewed-on: #60
2026-09-02 10:00:55 +00:00
jiantw83 3230d7c49d chore(manifest): 三份 plugin 資訊檔的版本號提升一階
What
- `plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` 的版本號同步提升一個修訂號。

Why
- 本輪改了五個範本與四支技能的內文,安裝端要靠版本號才判得出手上的快取是舊的。
- 三份資訊檔的版本號必須一致,任一份沒跟上,不同 CLI 會各自認到不同版本。

How
- 三份檔案只動版本號那一個欄位,其餘內容不變。

Who
- 各支 CLI 的安裝與更新流程,以及版本守門檢查。
2026-09-02 17:21:10 +08:00
jiantw83 b7b1d8bb01 docs(skills): 四支階段技能的目錄頁讀取與寫入敘述同步條列版面
What
- `skills/plan`、`skills/analyze`、`skills/implement`、`skills/maintain`:目錄頁的讀取敘述改成從 H2 區塊取值,寫入敘述從「單列 upsert」改成單一 H2 區塊 upsert,鍵補上內容頁頁名這個引數,並註明第四個引數是區塊檔。
- `skills/maintain`:讀寫的鍵改成該存取庫的 `{owner}/{repo}`,因為這個型別沒有內容頁。
- `references/behaviors.md`:四支技能的關鍵步驟、外部呼叫與可驗證跡象同步,跡象從「留下那一列」改成留下那一個 H2 區塊。
- `references/consensus.md`:查已答問題那一條補上問答目錄頁也是條列式版面、要從區塊取值而不是表格列。
- `references/stage-report.md`:目錄頁也算寫入那一段補上「改動一個區塊也算寫過那一頁」,並統一用 `CONTENTS` 這個型別餵進去。
- `README.md`:四支技能的流程敘述與 wiki 規則段同步,並補上五個目錄頁的版面規則、鍵的落點與各頁鍵欄的正確序號。

Why
- 範本已經改成條列版面,技能內文還寫著「那一列」,執行時就會照舊敘述組出表格列,跟工具的單一區塊 upsert 對不上。
- 讀取端的敘述沒跟著改,技能會拿表格的解析方式去讀一頁條列,既有紀錄一筆都認不出來。
- 呼叫少帶鍵這個引數,工具無從判斷要換掉哪一個區塊,同一筆會被當成新的附加上去。
- 行為清單是稽核與驗證的比對基準,敘述沒跟上,稽核會拿舊描述判合規。

How
- 四支技能的呼叫一律寫成 `wiki-contents.sh upsert {TYPE} {鍵欄} "{鍵}" {區塊檔} [{範本}]`,各頁的鍵欄序號照線上那一頁實際的欄位排法寫定。
- 完成條件與可驗證跡象改用區塊的說法,連結範例改成 `- {欄位名}:[{頁名}]({連結})` 的形態。
- 只改敘述與說明,不動任何腳本;轉檔與 upsert 的實作在別的存取庫。

Who
- 本存取庫四支階段技能,以及讀這幾份說明檔決定共識判定與階段回報寫法的流程。
- 稽核與驗證流程改拿新的行為清單比對。
2026-09-02 17:21:10 +08:00
jiantw83 19d918f499 docs(templates): 五個目錄頁範本改成大標題加條列
What
- `templates/plan-contents.md`、`templates/analyze-contents.md`、`templates/repo-contents.md`、`templates/deliver-contents.md`、`templates/maintain-contents.md` 的版面從 markdown 表格改成一筆一個 H2 區塊,欄位改成標題底下的一層條列。
- 四個有內容頁的型別,H2 標題寫成該筆對應內容頁的實際頁名;`MAINTAIN` 沒有內容頁,標題改用該存取庫的 `{owner}/{repo}`。
- 各範本的引言補上版面段,並把寫入語意從「單列 upsert」改寫成單一區塊 upsert,末端的示範資料改成一個完整的 H2 區塊。
- 引言裡寫明鍵欄的語意與各頁的正確序號,並點出填錯的靜默後果。

Why
- 目錄頁是全部使用者共用的索引。表格一寬就得橫向捲、欄位一多就對不上表頭,而且併行寫入時只要有人少打一根豎線,整張表就散掉,別人那一筆跟著看不見。
- 條列式一筆一個區塊,寫入端只換自己那一塊,壞掉也只壞自己那一塊。
- `MAINTAIN` 整個型別只有目錄頁,硬套內容頁頁名當標題會指向一個不存在的頁;存取庫名不會漂移,當鍵一樣穩。
- 鍵欄填錯時工具不會報錯,既有那一筆會被當成新的附加到頁尾,同一筆變兩個區塊,舊區塊從此再也更新不到,所以範本要把序號寫死。

How
- 一頁固定三段:H1 頁名、`>` 引言、然後每一筆一個 H2 區塊;欄位格式 `- {欄位名}:{值}`,全形冒號,順序照原本的欄位從左到右,鍵那一欄照樣留一條。
- 寫入示例統一成 `wiki-contents.sh upsert {TYPE} {鍵欄} "{鍵}" {區塊檔} [{本範本}]`,並註明第四個引數是整個 H2 區塊的 markdown,不是列檔。
- 頁上不留任何 markdown 表格;內容頁範本不在本輪範圍,維持圖表優先。

Who
- 影響照這五個範本寫目錄頁的四支階段技能。
- 舊表格頁的自動轉檔與單一區塊 upsert 的實作不在本存取庫,本存取庫只提供範本與敘述。
2026-09-02 17:21:10 +08:00
admin 5ee9f50fb6 Merge pull request '收尾寫一筆 skill-end 事件,執行狀態才回報得到助理' (#59) from feat/status-report into develop
Reviewed-on: #59
2026-09-02 08:04:15 +00:00
jiantw83 109a53466c chore(plugin 版本): 三份 manifest 升版至 0.3.1 2026-09-02 16:01:18 +08:00
jiantw83 f38d1f3087 feat(狀態回報): 收尾寫一筆 skill-end 事件
現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾
步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在
原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
2026-09-02 16:01:18 +08:00
admin 0191e7ba8e Merge pull request '連結一律寫成 [文字](絕對網址),並在寫入前驗證連得到' (#58) from feat/link-verification/main into develop
Reviewed-on: #58
2026-09-02 06:46:24 +00:00
jiantw83 4697bf433f chore(plugin 版本): 三份 manifest 升版至 0.3.0 2026-09-02 14:27:18 +08:00
jiantw83 f4e489ceb4 feat(link): 連結一律寫成 [文字](絕對網址),寫入前先驗證連得到
取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與
「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上
看起來像普通文字或死連結,巡不到也修不了。

連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API,
不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把
好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁
整批判死。
2026-09-02 14:27:18 +08:00
admin fc4e4a00e2 Merge pull request 'feat(wiki): 五個目錄頁改走專用存取庫,補齊 wiki-url 退出碼分流' (#56) from feat/wiki-contents-repo/main into develop
Reviewed-on: #56
2026-09-02 03:28:09 +00:00
jiantw83 9e71474bfb feat(wiki): 五個目錄頁改走專用存取庫,補齊 wiki-url 退出碼分流
What:PLAN、ANALYZE、DELIVER、MAINTAIN、REPO 五個目錄頁改由 wiki-repo CONTENTS
解析並透過 wiki-contents.sh upsert 寫入,內容頁仍各走自己的型別。MAINTAIN 只有
目錄頁,整個型別都在專用存取庫。

Why:目錄頁與內容頁不再同庫,跨庫沒有 wiki 連結語法可用,一律改 wiki-url 的絕對
網址。原本四處取網址都沒有退出碼分流,5 被讀成空字串就寫出空連結,7 被讀成 4 就
把活著的頁當成沒寫成。

How:plan 與 analyze 會上階段鎖,而寫入閘門只看鎖不看路徑,所以流程要產的列檔與
暫存檔會被自己的閘門擋掉。兩支的限制段明寫這些檔一律用 heredoc 或 mktemp 產出。
wp-gate.sh 讀的是分析內容頁,維持走 ANALYZE,原地註明不得改成 CONTENTS。

Who:jsc-sdlc
2026-09-02 11:02:34 +08:00
admin d7b3684bb4 Merge pull request '放行 jsc-assist 的 marketplace 條目到預設分支' (#54) from chore/marketplace-assist-registry/main into develop
Reviewed-on: #54
2026-09-01 04:55:40 +00:00
admin fa92fc010c Merge pull request 'chore/marketplace-assist-registry/sync-copies' (#53) from chore/marketplace-assist-registry/sync-copies into chore/marketplace-assist-registry/main
Reviewed-on: #53
2026-09-01 04:53:56 +00:00
jiantw83 41982760d4 chore(marketplace): 把 jsc-assist 登錄進統一 marketplace
What:
- 兩份 marketplace 檔各加一個 jsc-assist 條目,來源網址指向 assist 存放庫。

Why:
- 準則要求每個 domain 存放庫都帶同一份 marketplace 檔,任何一個存放庫都能當註冊入口。副本之間只要有一份沒跟上,稽核就會報出不一致。
- 正本少了這個條目,各 CLI 的安裝指令就找不到 jsc-assist,這個 domain 等於發佈不出去。

How:
- 條目由 meta 的 sync-marketplace.sh 產生,同時寫進正本與每個 domain 存放庫的副本,寫完逐檔比對位元組。這一支存放庫的兩份副本就是那一輪的產物。
- 條目依名稱排序,縮排與非 ASCII 描述的處理都交給同一支腳本,不手改 JSON。
- 這一批是從最新的預設分支重新產生的。前一輪的分支基底早於監控頁型別那批改動,直接合併會把那些改動回退掉,所以整批重做而不是解衝突。

Who:
助理 domain 落地的註冊步驟在這個存放庫的同步。
2026-09-01 12:50:04 +08:00
jiantw83 d4b1878967 Merge pull request '收攏 maintain 技能的 frontmatter 語法修正' (#50) from feat/cli-hook-rewire/main into develop 2026-09-01 00:58:44 +00:00
jiantw83 591263a2af Merge pull request '修正 maintain 技能 SKILL.md frontmatter 的 YAML 純量語法' (#49) from feat/cli-hook-rewire/quote-description into feat/cli-hook-rewire/main 2026-09-01 00:56:13 +00:00
jiantw83 c7d9777d04 fix(frontmatter): 修正 maintain 技能 SKILL.md frontmatter 的 YAML 純量語法錯誤
What:
- 修正 skills/maintain/SKILL.md frontmatter 裡 description 欄位的 YAML 語法錯誤。
- 整串 description 加上單引號,內部撇號改寫成兩個單引號,內容文字一個字都沒變。
- 同步更新 plugin.json、.claude-plugin/plugin.json、.codex-plugin/plugin.json 三個 manifest 版本號,從 0.2.7 進到 0.2.8。

Why:
- description 內含「冒號加空白」,屬於未加引號的 YAML plain scalar,違反 YAML 語法規定。
- Antigravity 解析 frontmatter 時當場中斷,整支技能被靜默丟棄,沒有任何錯誤訊息;磁碟上 34 支技能,Antigravity 只認得 28 支。
- 準則要求 description 用英文撰寫,不能把「: 」改成全形冒號迴避語法問題,只能加引號修正。

How:
- 整串 description 值加上單引號,內部撇號寫成兩個單引號跳脫,其餘字元不動。
- 用 git show HEAD: 取出改前的原始值,把改後的單引號純量還原後做字串相等比對,確認逐字相同、字元數一致。
- 執行 ste100-lint.sh、check-behaviors.sh、lint-frontmatter.sh 三支檢查腳本,退出碼皆為 0;git diff --numstat 顯示只動了 frontmatter 那一行。

Who:
- 本次修到 sdlc 技能組的 maintain 技能,屬 SDLC 維運階段、定期維護已交付專案的功能。
2026-08-31 19:04:17 +08:00
jiantw83 e0e857b84a Merge pull request '收攏 sdlc 四支技能的行為清單,功能主幹併回 develop' (#47) from feat/skill-behaviors-and-version-block/main into develop 2026-08-31 08:10:41 +00:00
24 changed files with 533 additions and 142 deletions
+8
View File
@@ -13,6 +13,14 @@
}, },
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)" "description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
}, },
{
"name": "jsc-assist",
"source": {
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
},
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
},
{ {
"name": "jsc-cli", "name": "jsc-cli",
"source": { "source": {
+8
View File
@@ -13,6 +13,14 @@
}, },
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)" "description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
}, },
{
"name": "jsc-assist",
"source": {
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
},
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
},
{ {
"name": "jsc-cli", "name": "jsc-cli",
"source": { "source": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-sdlc", "name": "jsc-sdlc",
"version": "0.2.7", "version": "0.3.2",
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)", "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)",
"skills": "./skills", "skills": "./skills",
"author": { "author": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-sdlc", "name": "jsc-sdlc",
"version": "0.2.7", "version": "0.3.2",
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)", "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)",
"skills": "./skills", "skills": "./skills",
"jsc": { "jsc": {
+11 -7
View File
@@ -1,6 +1,6 @@
# jsc-sdlc — 開發生命週期 # jsc-sdlc — 開發生命週期
jsc 技能組的 sdlc domain:規劃 → 分析 → 實作 → 維護四個階段,全程以 wiki 頁追蹤(`PLAN_CONTENTS`、`PLAN_{HASH}`、`ANALYZE_CONTENTS`、`ANALYZE_{HASH}`、`REPO_CONTENTS`、`REPO_{HASH}`、`DELIVER_CONTENTS`、`DELIVER_{HASH}`、`MAINTAIN_CONTENTS`)。工作包完成即交付:實作階段會詢問交付文件要產生成 `DELIVER_{HASH}` wiki 頁或 Gitea 議題留言。hook 或流程失敗記在異常頁(`ERROR_CONTENTS`、`ERROR_{HASH}`),那組頁面與範本由 `jsc-hooks` 擁有,本 domain 不放副本。每次切換階段先過模型閘門,判定全在程式層,由 `jsc-hooks/hooks/sdlc-gate.sh lock {stage}` 執行;各階段必要標籤、阻擋與回報的鐵則見 `references/model-gate.md`。分析前先與使用者確認來源分支;實作沿用同一條來源分支作為 worktree 基準與 PR 目標。**所有參考與來源分支一律取遠端的 `origin/{branch}`,動作前先 `git fetch --prune origin`;本地分支不可作為基準,本地與遠端不一致就停下回報。** jsc 技能組的 sdlc domain:規劃 → 分析 → 實作 → 維護四個階段,全程以 wiki 頁追蹤(`PLAN_CONTENTS`、`PLAN_{HASH}`、`ANALYZE_CONTENTS`、`ANALYZE_{HASH}`、`REPO_CONTENTS`、`REPO_{HASH}`、`DELIVER_CONTENTS`、`DELIVER_{HASH}`、`MAINTAIN_CONTENTS`)。**五個 `*_CONTENTS` 目錄頁住在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,內容頁各走自己的頁型變數,兩者不同存取庫**(詳見「環境變數」)。工作包完成即交付:實作階段會詢問交付文件要產生成 `DELIVER_{HASH}` wiki 頁或 Gitea 議題留言。hook 或流程失敗記在異常頁(`ERROR_CONTENTS`、`ERROR_{HASH}`),那組頁面與範本由 `jsc-hooks` 擁有,本 domain 不放副本。每次切換階段先過模型閘門,判定全在程式層,由 `jsc-hooks/hooks/sdlc-gate.sh lock {stage}` 執行;各階段必要標籤、阻擋與回報的鐵則見 `references/model-gate.md`。分析前先與使用者確認來源分支;實作沿用同一條來源分支作為 worktree 基準與 PR 目標。**所有參考與來源分支一律取遠端的 `origin/{branch}`,動作前先 `git fetch --prune origin`;本地分支不可作為基準,本地與遠端不一致就停下回報。**
## 安裝、更新、移除 ## 安裝、更新、移除
@@ -33,19 +33,19 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
### `plan` ### `plan`
規劃:讀計畫目錄 → 補充或新建計畫 → 決策樹持續提問,補全目標、範圍、可行性到達成共識(判定規則見 `references/consensus.md`,一輪不算問完)→ 產生使用者故事 → 寫回 `PLAN_{HASH}` → 階段回報(`tools/stage-report.sh`)。純邏輯,禁止程式碼與修改檔案;claude 另有 `jsc-hooks/hooks/write-guard.sh` 的階段寫入閘門把關,其餘四支 CLI 沒有 `PreToolUse`,只靠內文約束。工作包閘門對 `plan` 只提醒不擋(`analyze` 與 `maintain` 仍擋),放棄的是「手上工作包沒結清就別開新計畫」這道在製品上限。 規劃:讀計畫目錄 `PLAN_CONTENTS`(CONTENTS 存取庫)→ 補充或新建計畫 → 決策樹持續提問,補全目標、範圍、可行性到達成共識(判定規則見 `references/consensus.md`,一輪不算問完)→ 產生使用者故事 → 寫回 `PLAN_{HASH}`(PLAN 存取庫)→ 用 `jsc-gitea/tools/wiki-contents.sh` 把計畫目錄那個 `PLAN_{HASH}` 區塊 upsert,計畫頁連結用 `wiki-url` 的絕對網址 → 階段回報(`tools/stage-report.sh`)。純邏輯,禁止程式碼與修改檔案;claude 另有 `jsc-hooks/hooks/write-guard.sh` 的階段寫入閘門把關,其餘四支 CLI 沒有 `PreToolUse`,只靠內文約束。工作包閘門對 `plan` 只提醒不擋(`analyze` 與 `maintain` 仍擋),放棄的是「手上工作包沒結清就別開新計畫」這道在製品上限。
### `analyze` ### `analyze`
分析:先併行讀 `PLAN_CONTENTS` 與 `ANALYZE_CONTENTS`,沒有計畫可選就立刻停(不先問分支,因為分支隨計畫而變)→ 選定要擴充的分析或要分析的計畫 → 確認來源分支並檢查工作目錄與 `origin/{來源分支}` 一致 → 持續提問到達成共識(`references/consensus.md`)→ 搭配現況(工作目錄與 `REPO_{HASH}` 盤點複用)分析使用者故事 → WBS 產生編號工作包,`WP-01` 固定是獨立的交付、交接工作包,實作工作包相依於它 → CPM 估工時與天數 → 先產生**使用者故事驗收計畫**(每個情境標明 `真實資料` 或 `邏輯推論`,並寫出輸入、預期結果、資料來源與對應工作包)→ 再拆 TDD 待辦 → 寫回 `ANALYZE_{HASH}` → 階段回報(`tools/stage-report.sh`)。純邏輯,禁止程式碼與修改檔案;claude 另有 `jsc-hooks/hooks/write-guard.sh` 的階段寫入閘門把關,其餘四支 CLI 沒有 `PreToolUse`,只靠內文約束。 分析:先併行讀 `PLAN_CONTENTS` 與 `ANALYZE_CONTENTS`(兩頁都在 CONTENTS 存取庫),沒有計畫可選就立刻停(不先問分支,因為分支隨計畫而變)→ 選定要擴充的分析或要分析的計畫 → 確認來源分支並檢查工作目錄與 `origin/{來源分支}` 一致 → 持續提問到達成共識(`references/consensus.md`)→ 搭配現況(工作目錄與 `REPO_{HASH}` 盤點複用)分析使用者故事 → WBS 產生編號工作包,`WP-01` 固定是獨立的交付、交接工作包,實作工作包相依於它 → CPM 估工時與天數 → 先產生**使用者故事驗收計畫**(每個情境標明 `真實資料` 或 `邏輯推論`,並寫出輸入、預期結果、資料來源與對應工作包)→ 再拆 TDD 待辦 → 寫回 `ANALYZE_{HASH}`(ANALYZE 存取庫),再用 `jsc-gitea/tools/wiki-contents.sh` 把 `ANALYZE_CONTENTS` 與 `PLAN_CONTENTS` 各自那一個 H2 區塊 upsert,分析頁連結用 `wiki-url` 的絕對網址;重新盤點時 `REPO_{HASH}` 寫進 REPO 存取庫、`REPO_CONTENTS` 走同一支腳本 → 階段回報(`tools/stage-report.sh`)。純邏輯,禁止程式碼與修改檔案;claude 另有 `jsc-hooks/hooks/write-guard.sh` 的階段寫入閘門把關,其餘四支 CLI 沒有 `PreToolUse`,只靠內文約束。
### `implement` ### `implement`
實作:**閘門先跑,白工才不會發生**——先讀分析頁,把每個已開 PR 但沒合併的工作包批次跑 `tools/wp-gate.sh check`(各支 PR 互不相依,併發預取後才開始逐則問),動手前先跑 `tools/wp-gate.sh owns` 確認那支 PR 是自己這一包的,再依 `jsc-ask:ask` 決策樹與使用者對每一則留言達成共識才修(以 sub agent 回原 worktree、推同一條工作分支,不開第二個 PR),**這一步只結清舊 PR 的留言,不擋別的工作包**——每則處理過的留言都要用 `jsc-gitea/tools/gitea.sh comment-reply` 回覆,純討論或讚美才可忽略並在回報中列出理由,處理過的時間戳寫回分析頁**既有**的 PR 欄位當下一輪的 `--since` → 列出「未完成、無工作證」的候選工作包,沒有可挑的就停 → **提選項前先對每個候選各跑一次 `tools/wp-gate.sh check-deps --analyze ANALYZE_{HASH}`(候選之間可併行),只有 `ready` 排進選項**:活查它在分析頁上的相依工作包是否都已合併,只有相依於它的包才會被一支未合併的 PR 擋住,跟它無關的工作包可以平行進行(交付工作包排在選項最前)→ 領到包立刻跑 `tools/wp-gate.sh claim` 記下歸屬,**沒登錄成功就不得開工** → 確認來源分支(同時是 PR 目標):分析頁已記的值直接顯示並單鍵確認,只有「分析頁沒記錄」與「`origin/{來源分支}` 遠端不存在」兩種情況才走完整決策樹 → 產生工作證並寫回該工作包的「工作證」欄 → 交付工作包開工前先確認交付內容(API 文件、由使用者輸入,見 `references/deliver-formats.md`)→ **動程式碼前先從 `origin/{source-branch}` 建立 worktree**(`.worktree/{analysis-HASH}/{wp-number}/{repo}`,一個工作包一個 worktree,分支處理依決策樹詢問;多個存取庫的 worktree 併行建立)→ 在 worktree 內逐項 TDD 實作、每完成一項立即更新 wiki → **收尾稽核兩關同時啟動、並列進行,兩關都過才算工作包完成**:一關是 `jsc-review:code-review` 程式碼審查,另一關是 API 文件稽核——先跑 `jsc-review/tools/swagger-detect.sh` 判定專案支不支援 Swagger(退出碼 `0` 支援、`1` 不支援、`2` 參數或路徑有錯),支援才呼叫 `jsc-review:api-doc` 把控制器文件補到過關(沒過就以 sub agent 修完再稽核一次),不支援就**明確跳過並回報**,跳過算通過 → **每完成一個工作包就 commit、push、PR 回來源分支**(一包一 PR),接著 `tools/wp-gate.sh lock --wp` 上鎖並登記 PR 歸屬 → **用 `jsc-gitea/tools/pr-watch.sh` 盯到合併**(預設 60 秒輪詢、不自動退場;退出碼 `0` 已合併或關閉、`10` 有新留言就回頭跑同一套留言修正、`3` 查不到該 PR、`2` 參數或環境有問題),合併後解鎖並移除 worktree → 詢問交付文件格式(`DELIVER_{HASH}` wiki 頁或 Gitea 議題留言)並產出 → 詢問是否加入維護目錄 → 階段回報(`tools/stage-report.sh`,多報工作目錄與來源、工作、目標三條分支),PR 回報使用 `jsc-meta/references/pr-report.md` 的表格。**每完成一個任務就寫一筆工作日誌**(`jsc-log:worklog`):一個工作包、一輪 PR 留言修正、一個獨立的修正提交各算一個任務,不等到階段結束才補一次;`stage-report.sh --pending-file` 暫存的內容併進同一次寫入,寫入成功才清除。寫程式碼時註解只寫「為什麼這樣寫」,工作包編號、分析頁編號、待辦編號、分支名、PR 編號一律不寫進註解,完整清單與白名單見 `jsc-review` 的 `references/comment-scope.md`。 實作:**閘門先跑,白工才不會發生**——先讀分析頁,把每個已開 PR 但沒合併的工作包批次跑 `tools/wp-gate.sh check`(各支 PR 互不相依,併發預取後才開始逐則問),動手前先跑 `tools/wp-gate.sh owns` 確認那支 PR 是自己這一包的,再依 `jsc-ask:ask` 決策樹與使用者對每一則留言達成共識才修(以 sub agent 回原 worktree、推同一條工作分支,不開第二個 PR),**這一步只結清舊 PR 的留言,不擋別的工作包**——每則處理過的留言都要用 `jsc-gitea/tools/gitea.sh comment-reply` 回覆,純討論或讚美才可忽略並在回報中列出理由,處理過的時間戳寫回分析頁**既有**的 PR 欄位當下一輪的 `--since` → 列出「未完成、無工作證」的候選工作包,沒有可挑的就停 → **提選項前先對每個候選各跑一次 `tools/wp-gate.sh check-deps --analyze ANALYZE_{HASH}`(候選之間可併行),只有 `ready` 排進選項**:活查它在分析頁上的相依工作包是否都已合併,只有相依於它的包才會被一支未合併的 PR 擋住,跟它無關的工作包可以平行進行(交付工作包排在選項最前)→ 領到包立刻跑 `tools/wp-gate.sh claim` 記下歸屬,**沒登錄成功就不得開工** → 確認來源分支(同時是 PR 目標):分析頁已記的值直接顯示並單鍵確認,只有「分析頁沒記錄」與「`origin/{來源分支}` 遠端不存在」兩種情況才走完整決策樹 → 產生工作證並寫回該工作包的「工作證」欄 → 交付工作包開工前先確認交付內容(API 文件、由使用者輸入,見 `references/deliver-formats.md`)→ **動程式碼前先從 `origin/{source-branch}` 建立 worktree**(`.worktree/{analysis-HASH}/{wp-number}/{repo}`,一個工作包一個 worktree,分支處理依決策樹詢問;多個存取庫的 worktree 併行建立)→ 在 worktree 內逐項 TDD 實作、每完成一項立即更新 wiki → **收尾稽核兩關同時啟動、並列進行,兩關都過才算工作包完成**:一關是 `jsc-review:code-review` 程式碼審查,另一關是 API 文件稽核——先跑 `jsc-review/tools/swagger-detect.sh` 判定專案支不支援 Swagger(退出碼 `0` 支援、`1` 不支援、`2` 參數或路徑有錯),支援才呼叫 `jsc-review:api-doc` 把控制器文件補到過關(沒過就以 sub agent 修完再稽核一次),不支援就**明確跳過並回報**,跳過算通過 → **每完成一個工作包就 commit、push、PR 回來源分支**(一包一 PR),接著 `tools/wp-gate.sh lock --wp` 上鎖並登記 PR 歸屬 → **用 `jsc-gitea/tools/pr-watch.sh` 盯到合併**(預設 60 秒輪詢、不自動退場;退出碼 `0` 已合併或關閉、`10` 有新留言就回頭跑同一套留言修正、`3` 查不到該 PR、`2` 參數或環境有問題),合併後解鎖並移除 worktree → 詢問交付文件格式(`DELIVER_{HASH}` wiki 頁寫進 DELIVER 存取庫,或 Gitea 議題留言)並產出 → 詢問是否加入維護目錄;`DELIVER_CONTENTS` 與 `MAINTAIN_CONTENTS` 都在 CONTENTS 存取庫,一律用 `jsc-gitea/tools/wiki-contents.sh` 單一 H2 區塊 upsert,交付頁連結用 `wiki-url` 的絕對網址 → 階段回報(`tools/stage-report.sh`,多報工作目錄與來源、工作、目標三條分支),PR 回報使用 `jsc-meta/references/pr-report.md` 的表格。**每完成一個任務就寫一筆工作日誌**(`jsc-log:worklog`):一個工作包、一輪 PR 留言修正、一個獨立的修正提交各算一個任務,不等到階段結束才補一次;`stage-report.sh --pending-file` 暫存的內容併進同一次寫入,寫入成功才清除。寫程式碼時註解只寫「為什麼這樣寫」,工作包編號、分析頁編號、待辦編號、分支名、PR 編號一律不寫進註解,完整清單與白名單見 `jsc-review` 的 `references/comment-scope.md`。
### `maintain` ### `maintain`
維護:讀取維護期內的專案,**先整批併行 `git fetch` 並把每個專案切到 develop、master 並對齊 `origin/{branch}`**(專案之間互不相依),接著逐專案循序、每個專案一個 sub agent:提出至少五種維護方法,依決策樹讓使用者挑要做哪些 → commit / push / PR → 用 `jsc-meta/references/pr-report.md` 的表格回報 PR → **PR 開好立刻寫一筆工作日誌**(`jsc-log:worklog`,一個專案一筆,下一個專案開工前要先寫完)→ 更新前次維護時間 → 階段回報(`tools/stage-report.sh`)。sub agent 改程式碼時註解只寫「為什麼這樣寫」,議題編號、commit hash、分支名、人名與 `@` 提及一律不寫進註解,完整清單與白名單見 `jsc-review` 的 `references/comment-scope.md`;把關交給 `comment-scope.sh` 與提交前的 commit sweep,不再多做一輪人工全 diff 自查。**只有 claude 在寫檔當下就收到警告**;codex、copilot、antigravity 的 hook 要到回合或工作階段結束才掃,提交前的 commit sweep 是那三支唯一來得及的一道。僅適用於維護期內已交付的專案;尚在實作中或未登記於 `MAINTAIN_CONTENTS` 的專案不適用。 維護:從 `MAINTAIN_CONTENTS`(CONTENTS 存取庫;`MAINTAIN` 只有目錄頁)讀取維護期內的專案,**先整批併行 `git fetch` 並把每個專案切到 develop、master 並對齊 `origin/{branch}`**(專案之間互不相依),接著逐專案循序、每個專案一個 sub agent:提出至少五種維護方法,依決策樹讓使用者挑要做哪些 → commit / push / PR → 用 `jsc-meta/references/pr-report.md` 的表格回報 PR → **PR 開好立刻寫一筆工作日誌**(`jsc-log:worklog`,一個專案一筆,下一個專案開工前要先寫完)→ 用 `jsc-gitea/tools/wiki-contents.sh` 單一 H2 區塊 upsert 更新前次維護時間 → 階段回報(`tools/stage-report.sh`)。sub agent 改程式碼時註解只寫「為什麼這樣寫」,議題編號、commit hash、分支名、人名與 `@` 提及一律不寫進註解,完整清單與白名單見 `jsc-review` 的 `references/comment-scope.md`;把關交給 `comment-scope.sh` 與提交前的 commit sweep,不再多做一輪人工全 diff 自查。**只有 claude 在寫檔當下就收到警告**;codex、copilot、antigravity 的 hook 要到回合或工作階段結束才掃,提交前的 commit sweep 是那三支唯一來得及的一道。僅適用於維護期內已交付的專案;尚在實作中或未登記於 `MAINTAIN_CONTENTS` 的專案不適用。
<!-- JSC-SKILLS:END --> <!-- JSC-SKILLS:END -->
@@ -67,9 +67,13 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
## 環境變數 ## 環境變數
Wiki 位置:`PLAN_{HASH}` / `PLAN_CONTENTS` 只讀 `JSC_WIKI_REPO_PLAN`,再退回 `JSC_WIKI_REPO`;`ANALYZE_{HASH}` / `ANALYZE_CONTENTS` 只讀 `JSC_WIKI_REPO_ANALYZE`,再退回 `JSC_WIKI_REPO`;`REPO_{HASH}` / `REPO_CONTENTS` 只讀 `JSC_WIKI_REPO_REPO`,再退回 `JSC_WIKI_REPO`;`DELIVER_{HASH}` / `DELIVER_CONTENTS` 只讀 `JSC_WIKI_REPO_DELIVER`,再退回 `JSC_WIKI_REPO`;`MAINTAIN_CONTENTS` 只讀 `JSC_WIKI_REPO_MAINTAIN`,再退回 `JSC_WIKI_REPO`。不同類型不可互相代用;兩者都未設定才詢問(見 `jsc-gitea`)。 Wiki 位置:**目錄頁與內容頁分屬不同存取庫**。五個目錄頁 `PLAN_CONTENTS`、`ANALYZE_CONTENTS`、`REPO_CONTENTS`、`DELIVER_CONTENTS`、`MAINTAIN_CONTENTS` 一律由 `gitea.sh wiki-repo CONTENTS` 解析:只讀 `JSC_WIKI_REPO_CONTENTS`,再退回 `JSC_WIKI_REPO`,兩者都沒設定就 exit 3,**不退回各自的頁型變數**。內容頁各走自己的頁型:`PLAN_{HASH}` 只讀 `JSC_WIKI_REPO_PLAN`、`ANALYZE_{HASH}` 只讀 `JSC_WIKI_REPO_ANALYZE`、`REPO_{HASH}` 只讀 `JSC_WIKI_REPO_REPO`、`DELIVER_{HASH}` 只讀 `JSC_WIKI_REPO_DELIVER`,各自再退回 `JSC_WIKI_REPO`。`MAINTAIN` 是唯一只有目錄頁、沒有內容頁的型別,整個型別都在 CONTENTS 存取庫。不同類型不可互相代用;兩者都未設定才詢問(見 `jsc-gitea`)。
HASH 規則:`{owner}/{repo}` 的共用 wiki hash 一律由 `jsc-gitea/tools/hash-id` 計算(見 `jsc-gitea:wiki`),此 domain 不重複實作演算法。 目錄頁版面:五個目錄頁都是條列式,不放 markdown 表格。一頁固定三段——H1 頁名、`>` 引言、然後每一筆一個 H2 區塊。H2 標題就是那一筆的鍵,寫成對應內容頁的實際頁名——PLAN、ANALYZE、REPO、DELIVER 四型都取技能自己剛寫的那一頁的頁名(線上實際長成 `ANALYZE_20260821_100552_104F0709` 這樣,不是 `{TYPE}_` 加 40 碼的公式);`MAINTAIN` 沒有內容頁,標題改用該存取庫的 `{owner}/{repo}`,存取庫名不會漂移、當鍵一樣穩,硬造一個 `MAINTAIN_{HASH}` 會指向一個不存在的頁。標題不放連結、不加前後綴。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`,全形冒號,順序照該頁範本。
目錄頁寫入與連結:一律用 `jsc-gitea/tools/wiki-contents.sh upsert {TYPE} {鍵欄} {鍵} {區塊檔} {範本}` 單一區塊 upsert,禁止手工整頁覆蓋。參數語意:`{鍵欄}` 是舊表格裡持有內容頁連結那一欄的序號,只供自動轉檔用——該頁還是舊表格時,腳本從那一欄的連結網址取最後一段路徑當 H2 標題;該頁已經是條列版面就完全忽略它。它不是死參數,也不能隨便填:填錯會讓轉出來的標題跟鍵對不上,既有那一筆被當成新的附加上去,同一筆變成兩個區塊,舊區塊從此再也更新不到。五個頁型的正確值是 `PLAN 2`(計畫頁欄)、`ANALYZE 2`(分析頁欄)、`DELIVER 5`(交付頁欄)、`REPO 2`(盤點頁欄)、`MAINTAIN 1`(存取庫欄,`MAINTAIN` 沒有連結欄)。`{鍵}` 是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `{TYPE}_{HASH}` 的公式;`MAINTAIN` 沒有內容頁,鍵改用該存取庫的 `{owner}/{repo}`,硬造一個 `MAINTAIN_{HASH}` 會指向一個不存在的頁。`{區塊檔}` 是整個 H2 區塊的 markdown,不是列檔(結束碼 `0`=已更新或已新增、`1`=組不出頁面內容或寫入失敗、`2`=用法錯誤、`3`=CONTENTS 存取庫未設定、`4`=頁不存在且沒給範本、`7`=金鑰失效、`8`=其他 API 失敗)。連結一律寫成 `[{文字}]({連結})`,連結取自 `gitea.sh wiki-url` 的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,巡不到也修不了。寫進任何頁面前,每個連結先過 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入;有 DEAD 就不寫並回報(`1`=有連不到、`2`=沒給網址、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效要停下回報,不得當成連不到)。階段回報 `tools/stage-report.sh --page TYPE:PAGE` 的目錄頁一律填 `CONTENTS:` 這個型別,填舊型別會解到錯的存取庫、印不出網址。
HASH 規則:`{owner}/{repo}` 的共用 wiki hash 一律由 `jsc-gitea/tools/hash-id` 計算(見 `jsc-gitea:wiki`),此 domain 不重複實作演算法。長度與大小寫的正本也在那裡(現行為完整 40 碼大寫十六進位),本檔不複述規格,只要求一件事:`hash-id` 印出什麼就原樣用,不得自行截短。工作證 `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}` 不是 wiki 頁,但用的是同一支 `hash-id`,同樣原樣帶著走。
## 相關 domain ## 相關 domain
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-sdlc", "name": "jsc-sdlc",
"version": "0.2.7", "version": "0.3.2",
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)", "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)",
"skills": "./skills/", "skills": "./skills/",
"jsc": { "jsc": {
+19 -19
View File
@@ -6,38 +6,38 @@
| 項目 | 內容 | | 項目 | 內容 |
| --- | --- | | --- | --- |
| 觸發時機 | 計畫已經寫進 `PLAN_CONTENTS`、狀態是「未分析」,要把它拆成工作包時用。也用於延伸既有的 `ANALYZE_{HASH}` 分析頁。要寫程式碼時不用,那是 `implement`。`PLAN_CONTENTS` 還沒有計畫時不用,先跑 `plan`。 | | 觸發時機 | 計畫已經寫進 `PLAN_CONTENTS`(CONTENTS 存取庫)、該計畫 `## PLAN_{HASH}` 區塊的「狀態」是「未分析」,要把它拆成工作包時用。也用於延伸既有的 `ANALYZE_{HASH}` 分析頁。要寫程式碼時不用,那是 `implement`。`PLAN_CONTENTS` 還沒有計畫時不用,先跑 `plan`。 |
| 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock analyze` 過模型閘門,本階段要 `reasoning-max` 標籤、並行讀 `PLAN_CONTENTS` 與 `ANALYZE_CONTENTS`、讓使用者選延伸既有分析或分析新計畫、跑 `git fetch --prune origin` 後確認來源分支,並核對 HEAD 與 `origin/{source-branch}` 指到同一個 commit,有落差就停下回報、逐則使用者故事問到共識、先查 `REPO_{HASH}` 盤點頁決定複用,資料過期就開 sub agent 重新盤點並回寫 `REPO_{HASH}` 與 `REPO_CONTENTS`、做 WBS 拆工作包並標相依,交付工作包固定編為 `WP-01` 且獨立不併入實作包、用 CPM 估工時與天數,標出要徑並依 `references/cpm-chart.md` 畫 mermaid 甘特圖、寫使用者故事驗收計畫,再逐包寫 TDD 待辦、一次寫入 `ANALYZE_{HASH}`,並把 `ANALYZE_CONTENTS` 與 `PLAN_CONTENTS` 各自單列 upsert、跑 `tools/stage-report.sh analyze` 收尾回報。 | | 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock analyze` 過模型閘門,本階段要 `reasoning-max` 標籤、用 `gitea.sh wiki-repo CONTENTS` 解出目錄頁存取庫,並行讀 `PLAN_CONTENTS` 與 `ANALYZE_CONTENTS`,兩頁都是條列式版面:一個 H2 區塊一筆,標題就是對應的內容頁頁名,欄位是標題底下的一層條列,狀態看 `- 狀態:` 那一條,不是讀表格的列、讓使用者選延伸既有分析或分析新計畫,選項一個 H2 區塊組一個,標題給頁名與 HASH、說明文字取自該區塊的欄位條列、跑 `git fetch --prune origin` 後確認來源分支,並核對 HEAD 與 `origin/{source-branch}` 指到同一個 commit,有落差就停下回報、逐則使用者故事問到共識、先查 `REPO_{HASH}` 盤點頁(雜湊取自該存取庫自己的 `{owner}/{repo}`)決定複用,資料過期就開 sub agent 重新盤點,把 `REPO_{HASH}` 寫回 REPO 存取庫、`REPO_CONTENTS` 用 `wiki-contents.sh upsert REPO 2 {盤點頁實際頁名}` 寫回那一個 H2 區塊、做 WBS 拆工作包並標相依,交付工作包固定編為 `WP-01` 且獨立不併入實作包、用 CPM 估工時與天數,標出要徑並依 `references/cpm-chart.md` 畫 mermaid 甘特圖、寫使用者故事驗收計畫,再逐包寫 TDD 待辦、一次寫入 `ANALYZE_{HASH}`(ANALYZE 存取庫),再用 `wiki-contents.sh upsert ANALYZE 2 {分析頁實際頁名}` 與 `wiki-contents.sh upsert PLAN 2 {計畫頁實際頁名}` 各自單一 H2 區塊 upsert,第三個參數是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名(線上長成 `ANALYZE_20260821_100552_104F0709` 這樣),不套 `{TYPE}_{HASH}` 的公式,`2` 與 `2` 分別是舊表格「分析頁」欄與「計畫頁」欄的序號,也就是持有內容頁連結那一欄,只供自動轉檔用(該頁還是表格時從那一欄的連結網址取最後一段路徑當標題,已是條列就忽略),填錯會讓標題跟鍵對不上、同一筆變兩個區塊、舊區塊再也更新不到,第四個參數是整個區塊的檔案不是列檔、目錄區塊的分析頁與盤點頁連結一律取自 `gitea.sh wiki-url` 的絕對網址,並依退出碼分流(`0` 用它印出的網址,`4` 回頭補寫那頁再回來,`5`、`7`、`8` 停下回報,一律不自行組網址、也不留空白連結)、每個要放進頁面或目錄區塊的連結一律寫成 `[{文字}]({連結})`,不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`,而且寫入前先整批交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫,有 DEAD 就整筆不寫並回報連不到的清單(`2` 補參數重跑、`3` 先設好 `GITEA_HOST`、`7` 金鑰失效停下回報,不得當成連不到)、目錄區塊檔與待寫日誌檔一律用 Bash 的 heredoc 或 `mktemp` 產在暫存目錄,不走 Write 或 Edit 工具,因為 `write-guard.sh` 的 stage 模式只看階段鎖不看路徑,走工具會被自己的閘門擋下、跑 `tools/stage-report.sh analyze` 收尾回報,目錄頁一律用 `--page CONTENTS:{頁名}`、階段回報之後緊接著跑 `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:analyze {status} {結束碼} [detail]` 寫一筆結束事件,模型閘門擋下或 HEAD 與 `origin/{source-branch}` 有落差填 `blocked`,分析頁寫成功但目錄區塊沒跟上填 `degraded`,找不到腳本就安靜跳過,這一步失敗不改變本次階段的結論。 |
| 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-hooks/hooks/write-guard.sh`(claude 的 `PreToolUse` 擋寫入)、`jsc-gitea:wiki` 與 `jsc-gitea/tools/hash-id`、`jsc-ask:ask`、`tools/stage-report.sh`、`git fetch --prune origin`、`git branch -r`、`git rev-list --left-right --count`。 | | 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-hooks/hooks/write-guard.sh`(claude 的 `PreToolUse` 擋寫入)、`jsc-gitea:wiki`、`jsc-gitea/tools/hash-id`、`jsc-gitea/tools/gitea.sh` 的 `wiki-repo` 與 `wiki-url`、`jsc-gitea/tools/wiki-contents.sh upsert`、`jsc-gitea/tools/link-check.sh`、`jsc-ask:ask`、`tools/stage-report.sh`、`jsc-hooks/tools/report-status.sh skill-end`、`git fetch --prune origin`、`git branch -r`、`git rev-list --left-right --count`。 |
| 完成條件 | 模型閘門退出 0 並回報實際模型 id、來源分支經使用者確認且與遠端一致、每則使用者故事達成共識、每個複用決策連理由記進「複用決策」欄、每則故事對應至少一個編號工作包、每包有工時與天數、要徑與甘特圖齊備、每個實作包至少一則測試先行的 `[ ]` 待辦、`ANALYZE_{HASH}` 存進 wiki 且未決項欄有值(沒有就寫「無」)、目錄頁依 `wiki-get` 退出碼 0 與 4 分流寫入、`stage-report.sh` 的輸出原樣貼給使用者。提前停下也要跑收尾回報。 | | 完成條件 | 模型閘門退出 0 並回報實際模型 id、來源分支經使用者確認且與遠端一致、每則使用者故事達成共識、每個複用決策連理由記進「複用決策」欄、每則故事對應至少一個編號工作包、每包有工時與天數、要徑與甘特圖齊備、每個實作包至少一則測試先行的 `[ ]` 待辦、`ANALYZE_{HASH}` 存進 wiki 且未決項欄有值(沒有就寫「無」)、每個目錄區塊的頁面連結都來自退出 0 的 `wiki-url`,非 0 依 `4`、`5`、`7`、`8` 分流並講出退出碼、每一頁與每一個區塊寫出去之前都經 `link-check.sh` 退出 0 驗過,連結格式一律是 `[{文字}]({連結})`、每次目錄頁寫入都講出 `wiki-contents.sh` 的退出碼並依碼分流(`0` 續行,`1`、`3`、`7`、`8` 停下回報,`2` 修參數重跑,`4` 在本技能一律帶範本的呼叫方式下不會出現,真的出現就確認 plugin 安裝完整後重跑)、`stage-report.sh` 的輸出原樣貼給使用者、已寫一筆 `skill-end` 事件,狀態照 SKILL.md 收尾步驟那張對應表選定。提前停下也要跑收尾回報,結束事件一樣要寫;只有找不到 `report-status.sh` 才准沒有這一筆。 |
| 可驗證跡象 | wiki 上多一頁或更新一頁 `ANALYZE_{HASH}`;`ANALYZE_CONTENTS` 多一列該分析;`PLAN_CONTENTS` 該計畫那列狀態變成「已分析」;重新盤點時另外寫入 `REPO_{HASH}` 與 `REPO_CONTENTS` 一列;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;還沒有工作日誌時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔。工作目錄的檔案一律不動,程式碼沒有任何改動。 | | 可驗證跡象 | ANALYZE 存取庫多一頁或更新一頁 `ANALYZE_{HASH}`;CONTENTS 存取庫的 `ANALYZE_CONTENTS` 多一個 `## ANALYZE_{HASH}` 區塊,欄位逐條列出、連結是絕對網址;`PLAN_CONTENTS` 該計畫 `## PLAN_{HASH}` 區塊的「狀態」那一條變成「已分析」;重新盤點時 REPO 存取庫多一頁 `REPO_{HASH}`,`REPO_CONTENTS` 多一個 `## REPO_{HASH}` 區塊;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;還沒有工作日誌時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔;`stage-report.sh` 的「寫入的 wiki 頁」表格多一欄連結驗證,逐列標「通過、無可查端點、連不到、未驗證」,頁面上找不到 `[[...]]` 寫法的連結;`$JSC_HOME/usage/events.jsonl` 多一筆 `{kind:skill,phase:end}` 的事件,`name` 是 `jsc-sdlc:analyze`,`status` 是五個值之一。工作目錄的檔案一律不動,程式碼沒有任何改動。 |
## implement ## implement
| 項目 | 內容 | | 項目 | 內容 |
| --- | --- | | --- | --- |
| 觸發時機 | 分析頁已經有工作包與 TDD 待辦,要動手寫程式碼時用。也用於回頭處理既有工作包 PR 的留言。規劃或分析階段不用。`ANALYZE_CONTENTS` 沒有未完成分析頁時不用。 | | 觸發時機 | 分析頁已經有工作包與 TDD 待辦,要動手寫程式碼時用。也用於回頭處理既有工作包 PR 的留言。規劃或分析階段不用。`ANALYZE_CONTENTS` 上沒有任何狀態是「未完成」的 `## ANALYZE_{HASH}` 區塊時不用。 |
| 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock implement` 過模型閘門,本階段要 `coding` 標籤、讀 `ANALYZE_CONTENTS` 與每一頁未完成分析頁,這份資料後續步驟重用不再讀第二次、對每支未合併 PR 並行跑 `wp-gate.sh check` 取狀態與留言、動任何 PR 之前先跑 `wp-gate.sh owns` 確認歸屬,`foreign` 就放手、每則留言先經 `jsc-ask:ask` 取得共識,再由 sub agent 在同一個 worktree 內修、推同一條工作分支、逐則留言用 `gitea.sh comment-reply` 回覆處置結果,把 `latest=` 時間寫回分析頁 PR 欄、一輪留言修正算一個任務,當下寫一筆 `jsc-log:worklog`、列出可挑的工作包,每個候選並行跑 `wp-gate.sh check-deps` 過相依閘門,只有 `ready` 進選項、使用者挑定後跑 `wp-gate.sh claim` 登錄歸屬、確認來源分支,它同時是 worktree 基準與 PR 目標,遠端找不到就停下回報、產生 `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}` 工作證並寫回分析頁「工作證」欄,同時改工作階段名稱、交付包先問交付內容型別並寫回「交付型別」欄、`git fetch --prune origin` 後從 `origin/{source-branch}` 建 worktree,多存取庫並行建、逐項待辦各開一個 sub agent 跑 TDD,主代理每完成一項就把 `[ ]` 翻成 `[x]` 並存回 wiki、收尾並行跑 `jsc-review:code-review` 與 `jsc-review:api-doc`,後者先用 `swagger-detect.sh` 判定,退出碼 1 就明確略過並回報、跑 `jsc-git:pr` 開 PR 回來源分支,把 PR 連結寫回分析頁,再跑 `wp-gate.sh lock` 上鎖、寫一筆工作日誌,接著用 `pr-watch.sh` 輪詢等合併,退出碼 10 就回頭跑同一套留言處理、合併後移除 worktree 並把工作包標為完成、問交付格式並產出 `DELIVER_{HASH}` wiki 頁或 Gitea issue 留言、問要不要登錄維護並 upsert `MAINTAIN_CONTENTS`、跑 `tools/stage-report.sh implement` 收尾回報。 | | 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock implement` 過模型閘門,本階段要 `coding` 標籤、從 CONTENTS 存取庫讀 `ANALYZE_CONTENTS`,它是條列式版面:一個 H2 區塊一份分析,標題就是該筆分析頁的實際頁名(線上長成 `ANALYZE_20260821_100552_104F0709` 這樣,不是 `{TYPE}_{HASH}` 的公式),狀態看 `- 狀態:` 那一條,不是讀表格的列、再從 ANALYZE 存取庫讀每一頁未完成分析頁,WBS 表、PR 欄與待辦都在那張內容頁上、這份資料後續步驟重用不再讀第二次、對每支未合併 PR 並行跑 `wp-gate.sh check` 取狀態與留言、動任何 PR 之前先跑 `wp-gate.sh owns` 確認歸屬,`foreign` 就放手、每則留言先經 `jsc-ask:ask` 取得共識,再由 sub agent 在同一個 worktree 內修、推同一條工作分支、逐則留言用 `gitea.sh comment-reply` 回覆處置結果,把 `latest=` 時間寫回分析頁 PR 欄、一輪留言修正算一個任務,當下寫一筆 `jsc-log:worklog`、列出可挑的工作包,每個候選並行跑 `wp-gate.sh check-deps` 過相依閘門,只有 `ready` 進選項、使用者挑定後跑 `wp-gate.sh claim` 登錄歸屬、確認來源分支,它同時是 worktree 基準與 PR 目標,遠端找不到就停下回報、產生 `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}` 工作證並寫回分析頁「工作證」欄,同時改工作階段名稱、交付包先問交付內容型別並寫回「交付型別」欄、`git fetch --prune origin` 後從 `origin/{source-branch}` 建 worktree,多存取庫並行建、逐項待辦各開一個 sub agent 跑 TDD,主代理每完成一項就把 `[ ]` 翻成 `[x]` 並存回 wiki、收尾並行跑 `jsc-review:code-review` 與 `jsc-review:api-doc`,後者先用 `swagger-detect.sh` 判定,退出碼 1 就明確略過並回報、跑 `jsc-git:pr` 開 PR 回來源分支,把 PR 連結寫回分析頁,再跑 `wp-gate.sh lock` 上鎖、寫一筆工作日誌,接著用 `pr-watch.sh` 輪詢等合併,退出碼 10 就回頭跑同一套留言處理、合併後移除 worktree 並把工作包標為完成、問交付格式並產出 `DELIVER_{HASH}` wiki 頁(寫進 DELIVER 存取庫,雜湊取自 `{owner}/{repo}` 加工作包編號,一包一頁不互相覆蓋)或 Gitea issue 留言,交付頁再用 `wiki-contents.sh upsert DELIVER 5 {交付頁實際頁名}` 把 `DELIVER_CONTENTS` 那一個 H2 區塊寫回,第三個參數是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `{TYPE}_{HASH}` 的公式,`5` 是舊表格「交付頁」欄的序號,也就是持有內容頁連結那一欄,只供自動轉檔用(該頁還是表格時從那一欄的連結網址取最後一段路徑當標題,已是條列就忽略),填錯會讓標題跟鍵對不上、同一筆變兩個區塊、舊區塊再也更新不到,第四個參數是整個區塊的檔案不是列檔,連結取自 `gitea.sh wiki-url` 的絕對網址並依退出碼分流(`0` 用它印出的網址,`4` 回頭補寫交付頁再回來,`5`、`7`、`8` 停下回報,一律不自行組網址、也不留空白連結)、每個要放進頁面、目錄區塊、PR 欄或 issue 留言的連結一律寫成 `[{文字}]({連結})`,不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`,而且寫入前先整批交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫,有 DEAD 就整筆不寫並回報連不到的清單(`2` 補參數重跑、`3` 先設好 `GITEA_HOST`、`7` 金鑰失效停下回報,不得當成連不到)、問要不要登錄維護並用 `wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo}` 寫回 `MAINTAIN_CONTENTS` 那一個 H2 區塊,`MAINTAIN` 沒有內容頁,所以標題就是該存取庫的 `{owner}/{repo}`,不得硬造 `MAINTAIN_{HASH}` 指向一個不存在的頁,`1` 是舊表格「存取庫」欄的序號,也就是持有這一筆身分那一欄(該欄沒有連結,轉檔取純文字),只供自動轉檔用,填錯會讓標題跟鍵對不上、同一筆變兩個區塊、舊區塊再也更新不到、跑 `tools/stage-report.sh implement` 收尾回報,目錄頁一律用 `--page CONTENTS:{頁名}`、階段回報之後緊接著跑 `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:implement {status} {結束碼} [detail]` 寫一筆結束事件,模型閘門擋下、候選全被 `check-deps` 判 `blocked`、`owns` 判 `foreign` 或來源分支在遠端找不到都填 `blocked`,工作包已合併但交付目錄區塊或維護登錄沒寫成填 `degraded`,找不到腳本就安靜跳過,這一步失敗不改變本次階段的結論。 |
| 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`tools/wp-gate.sh` 的 `check`、`check-deps`、`claim`、`lock`、`owns`、`tools/stage-report.sh`、`jsc-gitea:wiki`、`jsc-gitea/tools/hash-id`、`jsc-gitea/tools/gitea.sh`(`comment-reply` 與 issue 留言 API)、`jsc-gitea/tools/pr-watch.sh`、`jsc-ask:ask`、`jsc-review:code-review`、`jsc-review:api-doc`、`jsc-review/tools/swagger-detect.sh`、`jsc-hooks/hooks/comment-scope.sh`、`jsc-git:commit`、`jsc-git:pr`、`jsc-log:worklog`、`git fetch`、`git worktree`。 | | 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`tools/wp-gate.sh` 的 `check`、`check-deps`、`claim`、`lock`、`owns`、`tools/stage-report.sh`、`jsc-gitea:wiki`、`jsc-gitea/tools/hash-id`、`jsc-gitea/tools/gitea.sh`(`wiki-repo`、`wiki-url`、`comment-reply` 與 issue 留言 API)、`jsc-gitea/tools/wiki-contents.sh upsert`、`jsc-gitea/tools/link-check.sh`、`jsc-gitea/tools/pr-watch.sh`、`jsc-ask:ask`、`jsc-review:code-review`、`jsc-review:api-doc`、`jsc-review/tools/swagger-detect.sh`、`jsc-hooks/hooks/comment-scope.sh`、`jsc-git:commit`、`jsc-git:pr`、`jsc-log:worklog`、`jsc-hooks/tools/report-status.sh skill-end`、`git fetch`、`git worktree`。 |
| 完成條件 | 模型閘門退出 0、每支未合併 PR 的留言都有處置與回覆、挑中的工作包經 `check-deps` 判為 `ready` 並 `claim` 成功、來源分支經確認並記進分析頁、工作證寫上 wiki、該包每一項待辦在 wiki 上都是 `[x]`、兩個收尾稽核都放行(`api-doc` 回報略過也算放行)、PR 開好且 `wp-gate.sh lock` 回 `status=locked`、工作日誌已寫、`pr-watch.sh` 退出 0 且 `wp-gate.sh check` 回 `status=merged`、交付文件已產出、維護登錄問題已回答、`stage-report.sh` 的輸出原樣貼出並列出 worktree 與三條分支。提前停下也要跑收尾回報。 | | 完成條件 | 模型閘門退出 0、每支未合併 PR 的留言都有處置與回覆、挑中的工作包經 `check-deps` 判為 `ready` 並 `claim` 成功、來源分支經確認並記進分析頁、工作證寫上 wiki、該包每一項待辦在 wiki 上都是 `[x]`、兩個收尾稽核都放行(`api-doc` 回報略過也算放行)、PR 開好且 `wp-gate.sh lock` 回 `status=locked`、工作日誌已寫、`pr-watch.sh` 退出 0 且 `wp-gate.sh check` 回 `status=merged`、交付文件已產出、交付頁那一條的連結來自退出 0 的 `wiki-url`,非 0 依 `4`、`5`、`7`、`8` 分流並講出退出碼、每一頁、每一個目錄區塊與每一則 issue 留言寫出去之前都經 `link-check.sh` 退出 0 驗過,連結格式一律是 `[{文字}]({連結})`、維護登錄問題已回答、每次目錄頁寫入都講出 `wiki-contents.sh` 的退出碼並依碼分流(`0` 續行,`1`、`3`、`7`、`8` 停下回報,`2` 修參數重跑,`4` 在本技能一律帶範本的呼叫方式下不會出現,真的出現就確認 plugin 安裝完整後重跑)、`stage-report.sh` 的輸出原樣貼出並列出 worktree 與三條分支、已寫一筆 `skill-end` 事件,狀態照 SKILL.md 收尾步驟那張對應表選定。提前停下也要跑收尾回報,結束事件一樣要寫;只有找不到 `report-status.sh` 才准沒有這一筆。 |
| 可驗證跡象 | 開出一條工作分支,並有一支回到來源分支的 PR;分析頁 `ANALYZE_{HASH}` 的「工作證」欄、「交付型別」欄、PR 欄、待辦勾選狀態都更新過;`DELIVER_{HASH}` wiki 頁或該 issue 下多一則留言;`DELIVER_CONTENTS` 多一列;使用者同意登錄時 `MAINTAIN_CONTENTS` 多一列;`LOG_{HASH}` 每完成一個任務多一筆條目;`$JSC_HOME/wp/{owner}-{repo}.claim` 與 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr` 兩個狀態檔;`{cwd}/.worktree/{分析-HASH}/{工作包編號}/{repo}` 目錄,PR 合併後被移除;該存取庫 `.git/info/exclude` 多一筆 `.worktree/`;PR 每則留言底下有回覆。 | | 可驗證跡象 | 開出一條工作分支,並有一支回到來源分支的 PR;ANALYZE 存取庫的分析頁 `ANALYZE_{HASH}` 的「工作證」欄(值是 `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}`,帶著 `hash-id` 原樣印出的雜湊)、「交付型別」欄、PR 欄、待辦勾選狀態都更新過;DELIVER 存取庫多一頁 `DELIVER_{HASH}`,或該 issue 下多一則留言;CONTENTS 存取庫的 `DELIVER_CONTENTS` 多一個 `## DELIVER_{HASH}` 區塊,欄位逐條列出、連結是絕對網址;使用者同意登錄時 `MAINTAIN_CONTENTS` 多一個 `## {owner}/{repo}` 區塊;`LOG_{HASH}` 每完成一個任務多一筆條目;`$JSC_HOME/wp/{owner}-{repo}.claim` 與 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr` 兩個狀態檔;`{cwd}/.worktree/{分析-HASH}/{工作包編號}/{repo}` 目錄,PR 合併後被移除;該存取庫 `.git/info/exclude` 多一筆 `.worktree/`;PR 每則留言底下有回覆;`stage-report.sh` 的「寫入的 wiki 頁」表格多一欄連結驗證,逐列標「通過、無可查端點、連不到、未驗證」,寫出去的頁面與留言都找不到 `[[...]]` 寫法的連結;`$JSC_HOME/usage/events.jsonl` 多一筆 `{kind:skill,phase:end}` 的事件,`name` 是 `jsc-sdlc:implement`,`status` 是五個值之一。 |
## maintain ## maintain
| 項目 | 內容 | | 項目 | 內容 |
| --- | --- | | --- | --- |
| 觸發時機 | 已交付、且已登錄在 `MAINTAIN_CONTENTS` 的專案要做定期保養時用。還在實作中的專案不用。沒有登錄進 `MAINTAIN_CONTENTS` 的專案不用。 | | 觸發時機 | 已交付、且在 `MAINTAIN_CONTENTS`(CONTENTS 存取庫)已有一個 `## {owner}/{repo}` 區塊的專案要做定期保養時用。還在實作中的專案不用。沒有登錄進 `MAINTAIN_CONTENTS` 的專案不用。 |
| 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock maintain` 過模型閘門,本階段不要求特定標籤,只要判定得出實際模型 id、讀 `MAINTAIN_CONTENTS`,篩出還在維護期內的專案(起始日不晚於今天,結束日為空或不早於今天)、主代理先並行對每個專案跑 `git fetch --prune origin`,切到維護分支並與 `origin/{branch}` 對齊,有落差就回報並略過該專案、之後一個專案一個專案跑,每個專案的維護都開一個 sub agent、每個專案提出至少五項維護做法給使用者挑、把改動用 `jsc-git:commit` 提交到新分支,推送後用 `jsc-git:pr` 開 PR 回步驟 3.1 那條分支、PR 開好當下寫一筆 `jsc-log:worklog`、把該專案在 `MAINTAIN_CONTENTS` 的「前次維護時間」更新成今天、主代理彙整每個專案的做法、PR 表格列與失敗原因、跑 `tools/stage-report.sh maintain` 收尾回報。 | | 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock maintain` 過模型閘門,本階段不要求特定標籤,只要判定得出實際模型 id、用 `gitea.sh wiki-repo CONTENTS` 解出存取庫後讀 `MAINTAIN_CONTENTS`(`MAINTAIN` 只有目錄頁、沒有內容頁),它是條列式版面:一個 `## {owner}/{repo}` 區塊一個專案,`MAINTAIN` 沒有內容頁,標題就是該存取庫的 `{owner}/{repo}`,存取庫與起始日、截止日都讀標題底下的欄位條列,不是讀表格的列、篩出還在維護期內的專案(起始日不晚於今天,結束日為空或不早於今天)、主代理先並行對每個專案跑 `git fetch --prune origin`,切到維護分支並與 `origin/{branch}` 對齊,有落差就回報並略過該專案、之後一個專案一個專案跑,每個專案的維護都開一個 sub agent、每個專案提出至少五項維護做法給使用者挑、把改動用 `jsc-git:commit` 提交到新分支,推送後用 `jsc-git:pr` 開 PR 回步驟 3.1 那條分支、PR 開好當下寫一筆 `jsc-log:worklog`、用 `wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo}` 把該專案在 `MAINTAIN_CONTENTS` 那個 H2 區塊的「前次維護時間」那一條更新成今天,標題與其餘欄位條列原樣保留,第三個參數的標題就是該專案的 `{owner}/{repo}`,取自讀進來的那個區塊,不得代換成 `MAINTAIN_{HASH}`,`1` 是舊表格「存取庫」欄的序號,也就是持有這一筆身分那一欄(該欄沒有連結,轉檔取純文字),只供自動轉檔用,填錯會讓標題跟鍵對不上、同一筆變兩個區塊、舊區塊再也更新不到,第四個參數是整個區塊的檔案不是列檔、該頁指向別型別頁的連結一律取自 `gitea.sh wiki-url` 的絕對網址並依退出碼分流(`0` 用它印出的網址,`4` 填「無」或回頭補寫那頁,`5`、`7`、`8` 停下回報,`7` 絕不當成 `4` 填「無」,一律不自行組網址、也不留空白連結)、每個要放進該區塊的連結一律寫成 `[{文字}]({連結})`,不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`,而且寫入前先整批交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫,有 DEAD 就整個區塊不寫並回報連不到的清單(`2` 補參數重跑、`3` 先設好 `GITEA_HOST`、`7` 金鑰失效停下回報,不得當成連不到)、主代理彙整每個專案的做法、PR 表格列與失敗原因、跑 `tools/stage-report.sh maintain` 收尾回報,目錄頁用 `--page CONTENTS:MAINTAIN_CONTENTS`、階段回報之後緊接著跑 `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:maintain {status} {結束碼} [detail]` 寫一筆結束事件,模型閘門擋下或在期專案全部與遠端有落差而整批略過填 `blocked`,一部分專案交出 PR、另一部分略過或「前次維護時間」沒更新成填 `degraded`,找不到腳本就安靜跳過,這一步失敗不改變本次階段的結論。 |
| 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-gitea:wiki`、`jsc-ask:ask`、`jsc-git:commit`、`jsc-git:pr`、`jsc-log:worklog`、`jsc-pkg:pkg-update`(選了套件更新才用)、`jsc-hooks/hooks/comment-scope.sh`、`tools/stage-report.sh`、`git fetch --prune origin`。 | | 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-gitea:wiki`、`jsc-gitea/tools/gitea.sh` 的 `wiki-repo` 與 `wiki-url`、`jsc-gitea/tools/wiki-contents.sh upsert`、`jsc-gitea/tools/link-check.sh`、`jsc-ask:ask`、`jsc-git:commit`、`jsc-git:pr`、`jsc-log:worklog`、`jsc-pkg:pkg-update`(選了套件更新才用)、`jsc-hooks/hooks/comment-scope.sh`、`tools/stage-report.sh`、`jsc-hooks/tools/report-status.sh skill-end`、`git fetch --prune origin`。 |
| 完成條件 | 模型閘門退出 0、步驟 2 列出的每個在期專案都跑完自己的 sub agent,各自收在一條 PR 連結或一個記錄下來的略過原因、每個完成的專案都有一筆工作日誌,而且下一個專案開始前就存好、`MAINTAIN_CONTENTS` 該專案的「前次維護時間」是今天,其他專案那幾列一個位元組都沒變、彙整報告涵蓋每個專案、`stage-report.sh` 的輸出原樣貼給使用者。沒有專案在期時,一樣要跑收尾回報。 | | 完成條件 | 模型閘門退出 0、步驟 2 列出的每個在期專案都跑完自己的 sub agent,各自收在一條 PR 連結或一個記錄下來的略過原因、每個完成的專案都有一筆工作日誌,而且下一個專案開始前就存好、`wiki-contents.sh` 退出 0 且退出碼有講出來(`1`、`3`、`7`、`8` 停下回報,`2` 修參數重跑,`4` 在本技能一律帶範本的呼叫方式下不會出現,真的出現就確認 plugin 安裝完整後重跑)、每個跨型別連結都來自退出 0 的 `wiki-url`,非 0 依 `4`、`5`、`7`、`8` 分流並講出退出碼、每一個區塊寫回去之前都經 `link-check.sh` 退出 0 驗過,連結格式一律是 `[{文字}]({連結})`、`MAINTAIN_CONTENTS` 該專案的「前次維護時間」是今天,其他專案那幾個區塊一個位元組都沒變、彙整報告涵蓋每個專案、`stage-report.sh` 的輸出原樣貼給使用者、已寫一筆 `skill-end` 事件,狀態照 SKILL.md 收尾步驟那張對應表選定。沒有專案在期時,一樣要跑收尾回報並寫結束事件,狀態填 `aborted`;只有找不到 `report-status.sh` 才准沒有這一筆。 |
| 可驗證跡象 | 每個維護過的專案多一條新分支與一支回到 `develop` 或 `master` 的 PR;`MAINTAIN_CONTENTS` 對應那列的「前次維護時間」變成今天;`LOG_{HASH}` 每個完成的專案多一筆條目;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;一個專案都沒完成時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔。 | | 可驗證跡象 | 每個維護過的專案多一條新分支與一支回到 `develop` 或 `master` 的 PR;CONTENTS 存取庫的 `MAINTAIN_CONTENTS` 對應那個 `## {owner}/{repo}` 區塊的「前次維護時間」那一條變成今天;`LOG_{HASH}` 每個完成的專案多一筆條目;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;一個專案都沒完成時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔;`stage-report.sh` 的「寫入的 wiki 頁」表格多一欄連結驗證,逐列標「通過、無可查端點、連不到、未驗證」,該頁找不到 `[[...]]` 寫法的連結;`$JSC_HOME/usage/events.jsonl` 多一筆 `{kind:skill,phase:end}` 的事件,`name` 是 `jsc-sdlc:maintain`,`status` 是五個值之一。 |
## plan ## plan
| 項目 | 內容 | | 項目 | 內容 |
| --- | --- | | --- | --- |
| 觸發時機 | 使用者要開新計畫、或要補強既有計畫時用。做分析或寫程式碼時不用。 | | 觸發時機 | 使用者要開新計畫、或要補強既有計畫時用。做分析或寫程式碼時不用。 |
| 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock plan` 過模型閘門,本階段要 `reasoning-max` 標籤、讀 `PLAN_CONTENTS`,列出狀態為「未分析」的計畫與各自的 HASH、讓使用者選延伸既有計畫或建立新計畫、依 `references/consensus.md` 跑決策樹,把目標、範圍、可行性三項問到共識,每個答案都要導出下一個問題、把共識寫成「身為⋯⋯我想要⋯⋯以便⋯⋯」格式的使用者故事、套 `templates/plan-page.md` 寫入 `PLAN_{HASH}`、把這份計畫在 `PLAN_CONTENTS` 的那一列 upsert,狀態填「未分析」、跑 `tools/stage-report.sh plan` 收尾回報。 | | 關鍵步驟 | 跑 `model-tags.sh sync` 與 `sdlc-gate.sh lock plan` 過模型閘門,本階段要 `reasoning-max` 標籤、用 `gitea.sh wiki-repo CONTENTS` 解出存取庫後讀 `PLAN_CONTENTS`,它是條列式版面:一個 H2 區塊一份計畫,標題就是該筆計畫頁的實際頁名(不是 `{TYPE}_{HASH}` 的公式),狀態看 `- 狀態:` 那一條,不是讀表格的列,列出狀態為「未分析」的計畫與各自的 HASH、讓使用者選延伸既有計畫或建立新計畫,選項一個 H2 區塊組一個,標題給頁名與 HASH、說明文字取自該區塊的欄位條列、依 `references/consensus.md` 跑決策樹,把目標、範圍、可行性三項問到共識,每個答案都要導出下一個問題、把共識寫成「身為⋯⋯我想要⋯⋯以便⋯⋯」格式的使用者故事、套 `templates/plan-page.md` 寫入 `PLAN_{HASH}`(PLAN 存取庫)、用 `wiki-contents.sh upsert PLAN 2 {計畫頁實際頁名}` 把這份計畫在 `PLAN_CONTENTS` 的那一個 H2 區塊 upsert,狀態那一條填「未分析」,第三個參數是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `{TYPE}_{HASH}` 的公式,`2` 是舊表格「計畫頁」欄的序號,也就是持有內容頁連結那一欄,只供自動轉檔用(該頁還是表格時從那一欄的連結網址取最後一段路徑當標題,已是條列就忽略),填錯會讓標題跟鍵對不上、同一筆變兩個區塊、舊區塊再也更新不到,第四個參數是整個區塊的檔案不是列檔、計畫頁連結取自 `gitea.sh wiki-url` 的絕對網址並依退出碼分流(`0` 用它印出的網址,`4` 回頭補寫計畫頁再回來,`5`、`7`、`8` 停下回報,一律不自行組網址、也不留空白連結)、每個要放進頁面或目錄區塊的連結一律寫成 `[{文字}]({連結})`,不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`,而且寫入前先整批交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫,有 DEAD 就整筆不寫並回報連不到的清單(`2` 補參數重跑、`3` 先設好 `GITEA_HOST`、`7` 金鑰失效停下回報,不得當成連不到)、目錄區塊檔與待寫日誌檔一律用 Bash 的 heredoc 或 `mktemp` 產在暫存目錄,不走 Write 或 Edit 工具,因為 `write-guard.sh` 的 stage 模式只看階段鎖不看路徑,走工具會被自己的閘門擋下、跑 `tools/stage-report.sh plan` 收尾回報,目錄頁用 `--page CONTENTS:PLAN_CONTENTS`、階段回報之後緊接著跑 `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:plan {status} {結束碼} [detail]` 寫一筆結束事件,模型閘門擋下填 `blocked`,計畫頁寫成功但 `PLAN_CONTENTS` 那個區塊沒跟上填 `degraded`,找不到腳本就安靜跳過,這一步失敗不改變本次階段的結論。 |
| 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-hooks/hooks/write-guard.sh`(claude 的 `PreToolUse` 擋寫入)、`jsc-gitea:wiki` 與 `jsc-gitea/tools/hash-id`、`jsc-ask:ask`、`tools/stage-report.sh`。 | | 外部呼叫 | `jsc-cli/tools/model-tags.sh sync`、`jsc-hooks/hooks/sdlc-gate.sh lock`、`jsc-hooks/hooks/write-guard.sh`(claude 的 `PreToolUse` 擋寫入)、`jsc-gitea:wiki`、`jsc-gitea/tools/hash-id`、`jsc-gitea/tools/gitea.sh` 的 `wiki-repo` 與 `wiki-url`、`jsc-gitea/tools/wiki-contents.sh upsert`、`jsc-gitea/tools/link-check.sh`、`jsc-ask:ask`、`tools/stage-report.sh`、`jsc-hooks/tools/report-status.sh skill-end`。 |
| 完成條件 | 模型閘門退出 0 並回報實際模型 id、目標、範圍、可行性三項都達成共識,而且使用者明確確認過覆述的摘要、每個共識項目至少對應一則使用者故事、`PLAN_{HASH}` 存進 wiki 且範本要求的每個區段都有值、沒有留下未填的佔位字、`PLAN_CONTENTS` 該列狀態是「未分析」,其他列一個位元組都沒變,而且寫入時分流的 `wiki-get` 退出碼有講出來、`stage-report.sh` 的輸出原樣貼給使用者。提前停下也要跑收尾回報。 | | 完成條件 | 模型閘門退出 0 並回報實際模型 id、目標、範圍、可行性三項都達成共識,而且使用者明確確認過覆述的摘要、每個共識項目至少對應一則使用者故事、`PLAN_{HASH}` 存進 wiki 且範本要求的每個區段都有值、沒有留下未填的佔位字、`PLAN_CONTENTS` 該區塊的狀態那一條是「未分析」,計畫頁那一條的連結來自退出 0 的 `wiki-url`(非 0 依 `4`、`5`、`7`、`8` 分流並講出退出碼),並經 `link-check.sh` 退出 0 驗過,格式是 `[{文字}]({連結})`,其他區塊一個位元組都沒變,而且 `wiki-contents.sh` 的退出碼有講出來並依碼分流(`0` 續行,`1`、`3`、`7`、`8` 停下回報,`2` 修參數重跑,`4` 在本技能一律帶範本的呼叫方式下不會出現,真的出現就確認 plugin 安裝完整後重跑)、`stage-report.sh` 的輸出原樣貼給使用者、已寫一筆 `skill-end` 事件,狀態照 SKILL.md 收尾步驟那張對應表選定。提前停下也要跑收尾回報,結束事件一樣要寫;只有找不到 `report-status.sh` 才准沒有這一筆。 |
| 可驗證跡象 | wiki 上多一頁或更新一頁 `PLAN_{HASH}`;`PLAN_CONTENTS` 多一列或更新一列,狀態是「未分析」;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;還沒有工作日誌時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔。工作目錄的檔案一律不動。 | | 可驗證跡象 | PLAN 存取庫多一頁或更新一頁 `PLAN_{HASH}`;CONTENTS 存取庫的 `PLAN_CONTENTS` 多一個或更新一個 `## PLAN_{HASH}` 區塊,狀態那一條是「未分析」、計畫頁那一條是 `[{文字}]({連結})` 格式的絕對網址;`$JSC_HOME/sessions/{工作階段 id}.stage` 是 `sdlc-gate.sh lock` 寫的階段鎖狀態檔;還沒有工作日誌時,`$JSC_HOME/worklog-pending/{HASH}/` 下有暫存的日誌內容檔;`stage-report.sh` 的「寫入的 wiki 頁」表格多一欄連結驗證,逐列標「通過、無可查端點、連不到、未驗證」,頁面上找不到 `[[...]]` 寫法的連結;`$JSC_HOME/usage/events.jsonl` 多一筆 `{kind:skill,phase:end}` 的事件,`name` 是 `jsc-sdlc:plan`,`status` 是五個值之一。工作目錄的檔案一律不動。 |
+1 -1
View File
@@ -6,7 +6,7 @@
- **每個回答都要生出下一個問題**:從使用者的答案往下推,找出它新暴露的未知,繼續問。答完一輪就收工是最常見的失敗。 - **每個回答都要生出下一個問題**:從使用者的答案往下推,找出它新暴露的未知,繼續問。答完一輪就收工是最常見的失敗。
- 問題一次只問一件事,選項一律標明影響範圍(依 `jsc-ask:ask`)。 - 問題一次只問一件事,選項一律標明影響範圍(依 `jsc-ask:ask`)。
- 問過的別再問:先查 wiki 的 `QUESTION_CONTENTS` 與 `QUESTION_{HASH}`,已答的直接沿用。 - 問過的別再問:先查 wiki 的 `QUESTION_CONTENTS` 與 `QUESTION_{HASH}`,已答的直接沿用。目錄頁 `QUESTION_CONTENTS` 是條列式版面,一個 H2 區塊一筆,標題就是對應的 `QUESTION_{HASH}` 頁名,欄位在標題底下逐條列出,不是表格的一列。
## 達成共識的兩個條件 ## 達成共識的兩個條件
+36 -4
View File
@@ -1,7 +1,8 @@
# 階段回報 — 四個階段收尾都要交的東西 # 階段回報 — 四個階段收尾都要交的東西
規劃、分析、實作、維護跑完,最後一件事一定是階段回報。彙整由 `tools/stage-report.sh` 產出, 規劃、分析、實作、維護跑完,最後兩件事固定是這兩筆:先給使用者看的階段回報,再給機器看的
技能只負責把事實餵進去:寫過哪些 wiki 頁、有沒有寫工作日誌、實作階段的工作目錄與三條分支。 結束事件。階段回報由 `tools/stage-report.sh` 彙整產出,技能只負責把事實餵進去:寫過哪些
wiki 頁、有沒有寫工作日誌、實作階段的工作目錄與三條分支。結束事件見本頁最後一節。
## 什麼時候回報 ## 什麼時候回報
@@ -17,7 +18,19 @@
| 所有寫入的 wiki 連結 | 每寫一頁就記一筆,收尾時用 `--page TYPE:PAGE` 全部餵進去 | 沒寫任何頁就據實回報「本階段沒有寫入任何 wiki 頁」 | | 所有寫入的 wiki 連結 | 每寫一頁就記一筆,收尾時用 `--page TYPE:PAGE` 全部餵進去 | 沒寫任何頁就據實回報「本階段沒有寫入任何 wiki 頁」 |
`--page` 的 `TYPE` 是頁名前綴(`PLAN`、`ANALYZE`、`DELIVER`、`REPO`、`MAINTAIN`、`LOG`)。腳本用它解析 `--page` 的 `TYPE` 是頁名前綴(`PLAN`、`ANALYZE`、`DELIVER`、`REPO`、`MAINTAIN`、`LOG`)。腳本用它解析
該類型的 wiki 存取庫,再換成絕對網址,所以跨存取庫的頁面也連得到。目錄頁(`*_CONTENTS`)也算寫入,要列。 該類型的 wiki 存取庫,再換成絕對網址,所以跨存取庫的頁面也連得到。目錄頁(`*_CONTENTS`)也算寫入,要列;
目錄頁是條列式版面,一個 H2 區塊一筆,改動一個區塊也算寫過那一頁,一律用 `--page CONTENTS:{頁名}` 餵進去。
## 連結驗證
連結一律寫成 `[{文字}]({連結})`,網址取自 `jsc-gitea/tools/gitea.sh wiki-url`,不自行組路徑,
也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`。寫進任何頁面之前,每個連結先交給
`jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入;有 DEAD 就不寫,把連不到的清單回報給使用者。
`stage-report.sh` 印出來的那張連結清單也會再驗一次,逐列標「通過、無可查端點、連不到、未驗證」。
這一道是複查,不是上面那道關卡的替代品:頁面在收尾之前就寫完了,所以這裡只註記、不阻擋。
金鑰失效(`link-check.sh` 結束碼 7)標成「未驗證」,不標成「連不到」——把金鑰問題寫成死連結,
下一手就會照著去刪還活著的頁。
## 實作階段多回報四項 ## 實作階段多回報四項
@@ -47,6 +60,25 @@
| 碼 | 意思 | 呼叫端要做的事 | | 碼 | 意思 | 呼叫端要做的事 |
| --- | --- | --- | | --- | --- | --- |
| 0 | 回報完整 | 把輸出原樣貼給使用者 | | 0 | 回報完整 | 把輸出原樣貼給使用者 |
| 1 | 有警告(缺工作日誌、或實作階段沒有 PR) | 一樣把輸出貼給使用者。**這是警告不是阻擋**,階段的工作已經做完了 | | 1 | 有警告(缺工作日誌、實作階段沒有 PR、或清單裡有連不到的連結) | 一樣把輸出貼給使用者,連不到的連結照著修。**這是警告不是阻擋**,階段的工作已經做完了 |
| 2 | 用法錯誤 | 修正參數重跑 | | 2 | 用法錯誤 | 修正參數重跑 |
| 3 | 找不到 `gitea.sh` 或 `sdlc-gate.sh` | 修好相依關係再重跑 | | 3 | 找不到 `gitea.sh` 或 `sdlc-gate.sh` | 修好相依關係再重跑 |
## 結束事件 — 階段回報之後那一筆
階段回報是給人看的,結束事件是給機器看的。跑完階段回報,緊接著跑
`jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:{技能名} {status} {結束碼} [detail]`,
一筆寫進 `$JSC_HOME/usage/events.jsonl`。腳本名怎麼寫,比照各技能既有寫
`jsc-hooks/hooks/sdlc-gate.sh` 的方式,不另立一套。
為什麼非得由技能自己寫:配對的 `skill-start` 由 jsc-hooks 自己記,但那個 hook 接在技能工具
呼叫之後就觸發,實際工作還在後面的模型輪次,所以**沒有任何 hook 看得到階段怎麼結束**。
有 `start` 沒有配對的 `end`,在紀錄裡就是中止;收尾少寫這一筆,跑完的階段每一次都會被算成中止。
| 項目 | 規則 |
| --- | --- |
| `status` | `ok`、`blocked`、`failed`、`degraded`、`aborted` 五選一。哪一種情況選哪一個,各技能 SKILL.md 的收尾步驟有自己的對應表,那張表是唯一判準 |
| 模型閘門擋下 | 一律 `blocked`,不是 `failed`。閘門擋下不合格的模型是閘門在做事,記成失敗會讓下一手去找一個不存在的缺陷 |
| `{結束碼}` | 判定該狀態的那支腳本的結束碼;沒有任何腳本回非 0 就填 `0`,`ok` 與 `aborted` 都是這種 |
| `[detail]` | 選填,繁體中文單行,講清楚是什麼決定了這個狀態。腳本截到 200 字元,長內容不要塞 |
| 失敗怎麼辦 | **這一步失敗不改變本次階段的結論。** 找不到腳本就安靜跳過,不回報也不重跑任何步驟。三個記錄子命令本來就設計成寫檔失敗也回 0,所以回非 0 只代表呼叫本身寫錯了(`2` 是用法錯誤),修一次參數就好 |
+72 -18
View File
@@ -1,6 +1,6 @@
--- ---
name: analyze name: analyze
description: SDLC analysis stage. Gate on capability tags enforced in code by sdlc-gate (analyze requires reasoning-max), pick a plan from PLAN_CONTENTS first, then confirm that plan's source branch and question until consensus per references/consensus.md. Analyze its user stories against the current state (working directory plus REPO_{HASH} inventory for reuse), then run WBS with a standalone delivery/handover WP-01, CPM estimates and TDD todos. Write wiki page ANALYZE_{HASH} with real sample data, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written; logic only - never write code or modify files. Use after planning and before implementation; not for writing code (that is implement), and not before a plan exists in PLAN_CONTENTS. description: SDLC analysis stage. Gate on capability tags enforced in code by sdlc-gate (analyze requires reasoning-max), pick a plan from PLAN_CONTENTS first, then confirm that plan's source branch and question until consensus per references/consensus.md. Analyze its user stories against the current state (working directory plus REPO_{HASH} inventory for reuse), then run WBS with a standalone delivery/handover WP-01, CPM estimates and TDD todos. Write wiki page ANALYZE_{HASH} with real sample data into the ANALYZE wiki repo, upsert every directory H2 block (ANALYZE_CONTENTS, PLAN_CONTENTS, REPO_CONTENTS - all in the separate CONTENTS repo) through jsc-gitea/tools/wiki-contents.sh with absolute wiki-url links, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written; logic only - never write code or modify files. Use after planning and before implementation; not for writing code (that is implement), and not before a plan exists in PLAN_CONTENTS.
--- ---
# analyze # analyze
@@ -8,14 +8,17 @@ description: SDLC analysis stage. Gate on capability tags enforced in code by sd
Goal: create or extend the wiki analysis page `ANALYZE_{HASH}`. Goal: create or extend the wiki analysis page `ANALYZE_{HASH}`.
This skill is a **logic-only** stage: never output code, and **never modify any file**. This skill is a **logic-only** stage: never output code, and **never modify any file**.
`{HASH}` = the shared wiki hash for `{owner}/{repo}` used to build the `ANALYZE_{HASH}` page name, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). `{HASH}` = the shared wiki hash for `{owner}/{repo}`, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). It prints the **full 40-character uppercase SHA-1** of its input — no truncation to 8 characters, no prefix rewrite. Never shorten it by hand: a shortened name points at a page nobody else writes to. `REPO_{HASH}` runs the same command over that repository's own `{owner}/{repo}`, so a multi-repository analysis holds one inventory page per repository.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Step 11 still runs after such a stop.
**Content pages and directory pages live in different wiki repos.** `ANALYZE_{HASH}` sits in the repo `jsc-gitea/tools/gitea.sh wiki-repo ANALYZE` resolves, `REPO_{HASH}` in the one `gitea.sh wiki-repo REPO` resolves. All three directory pages — `ANALYZE_CONTENTS`, `PLAN_CONTENTS` and `REPO_CONTENTS` — sit in the repo `gitea.sh wiki-repo CONTENTS` resolves: `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to `JSC_WIKI_REPO_ANALYZE`, `JSC_WIKI_REPO_PLAN` or `JSC_WIKI_REPO_REPO`. **All three are bullet-list directory pages, not tables**: one H2 block per entry, the heading being that entry's content page **actual** name — the page the run itself wrote, which on the live wiki looks like `ANALYZE_20260821_100552_104F0709`, never a `{TYPE}_{HASH}` formula — and the fields a one-level bullet list under it, each written `- {欄位名}:{值}` in the order that page's template gives. Every directory entry links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`, written as `[{text}]({url})` — one link syntax, whichever wiki the two pages sit in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Steps 11 and 12 still run after such a stop.
## Steps ## Steps
1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock analyze`. This stage requires the `reasoning-max` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict. 1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock analyze`. This stage requires the `reasoning-max` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict.
2. **Find out whether there is anything to analyze, before anything else costs the user a round.** Read `PLAN_CONTENTS` for plans whose status is the literal 「未分析」 (name and HASH), and read `ANALYZE_CONTENTS` for existing analyses. **The two pages are independent — read them concurrently, and do not wait for the source branch: which branch the analysis reads from depends on the plan, so it is confirmed in step 4, after the target is known.** Completion condition: you have listed every 未分析 plan with its name and HASH plus every existing analysis, or reported that both lists are empty and stopped. 2. **Find out whether there is anything to analyze, before anything else costs the user a round.** Both directory pages come out of the CONTENTS wiki repo (`gitea.sh wiki-repo CONTENTS`, resolved once and reused). Read `PLAN_CONTENTS` for plans whose status is the literal 「未分析」 (name and HASH), and read `ANALYZE_CONTENTS` for existing analyses. **Read both as H2 blocks, not table rows**: on `PLAN_CONTENTS` each `## PLAN_{HASH}` heading is one plan, and a plan is 未分析 when its `- 狀態:` bullet reads 「未分析」; on `ANALYZE_CONTENTS` each `## ANALYZE_{HASH}` heading is one analysis, with its 計畫名稱、分析頁、HASH、工作包、未完成項目、狀態 as the bullets under it. **The two pages are independent — read them concurrently, and do not wait for the source branch: which branch the analysis reads from depends on the plan, so it is confirmed in step 4, after the target is known.** Completion condition: you have listed every 未分析 plan with its name and HASH plus every existing analysis, or reported that both lists are empty and stopped.
3. Let the user choose per `jsc-ask:ask` rules: **extend an existing analysis** or **analyze a new plan**. State the impact scope on every option. Completion condition: the user has picked one option explicitly, and you have named the target — the existing `ANALYZE_{HASH}` page, or the plan the new analysis covers. 3. Let the user choose per `jsc-ask:ask` rules: **extend an existing analysis** or **analyze a new plan**. **Build each option from one H2 block**: the heading gives the page name and the HASH, and the option text comes from that block's bullets — 計畫名稱 plus 未完成項目 and 狀態 for an existing analysis, 計畫名稱 plus 建立時間 for a plan. State the impact scope on every option. Completion condition: the user has picked one option explicitly, and you have named the target — the existing `ANALYZE_{HASH}` page, or the plan the new analysis covers.
4. **Confirm the source branch for that target** — the branch whose code counts as the current state, confirmed before any code is read and before the analysis page is written: 4. **Confirm the source branch for that target** — the branch whose code counts as the current state, confirmed before any code is read and before the analysis page is written:
1. Run `git fetch --prune origin` first — without it, every `origin/...` reference is stale cache. Then report the working directory's current branch, the **remote** branches available (`git branch -r`; never `git branch`) and whether the working tree is clean. 1. Run `git fetch --prune origin` first — without it, every `origin/...` reference is stale cache. Then report the working directory's current branch, the **remote** branches available (`git branch -r`; never `git branch`) and whether the working tree is clean.
2. Ask per `jsc-ask:ask` rules which branch the analysis reads from; state the impact scope on every option (analysing the wrong branch produces work packages for code that does not exist). 2. Ask per `jsc-ask:ask` rules which branch the analysis reads from; state the impact scope on every option (analysing the wrong branch produces work packages for code that does not exist).
@@ -24,9 +27,9 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
5. Analyze the plan page's user stories one by one against the **current state**, **questioning until consensus** per `references/consensus.md` (the single authority for both planning and analysis): every answer produces the next question, and consensus needs both no output-changing unknown **and** the user's explicit confirmation. Never assume a missing detail, and never start the WBS while any item is still open. Current state means: 5. Analyze the plan page's user stories one by one against the **current state**, **questioning until consensus** per `references/consensus.md` (the single authority for both planning and analysis): every answer produces the next question, and consensus needs both no output-changing unknown **and** the user's explicit confirmation. Never assume a missing detail, and never start the WBS while any item is still open. Current state means:
1. Every file in the working directory, at the commit `origin/{source-branch}` points to (verified in step 4). 1. Every file in the working directory, at the commit `origin/{source-branch}` points to (verified in step 4).
2. **Reuse an existing method or endpoint unless its logic cannot satisfy the requirement**: 2. **Reuse an existing method or endpoint unless its logic cannot satisfy the requirement**:
- Check the `REPO_{HASH}` inventory page first. Re-inventory when the feature or endpoint is missing, or when the recorded commit sha differs from the current one. - Check the `REPO_{HASH}` inventory page first, in the REPO wiki repo. **Its `{HASH}` is `hash-id` over that repository's own `{owner}/{repo}`** — one inventory page per repository, so a multi-repository analysis holds one `REPO_{HASH}` per repository and never one shared page. Re-inventory when the feature or endpoint is missing, or when the recorded commit sha differs from the current one.
- Re-inventory **MUST run as a sub agent**: analyze the repository's features and endpoints, attach the current commit sha, write back to `REPO_{HASH}` with `templates/repo-page.md`, and upsert this repository's row in `REPO_CONTENTS` per `templates/repo-contents.md` — add the row if missing, otherwise refresh its commit sha and 盤點時間. `REPO_CONTENTS` is a contents page: read it back, change only this repository's row, and write the whole page. Never overwrite it wholesale, and never touch a row belonging to another repository. - Re-inventory **MUST run as a sub agent**: analyze the repository's features and endpoints, attach the current commit sha, write back to `REPO_{HASH}` with `templates/repo-page.md`, then upsert this repository's H2 entry block in `REPO_CONTENTS` with `jsc-gitea/tools/wiki-contents.sh upsert REPO 2 {inventory page name} {entry file} templates/repo-contents.md`. The entry file holds the heading `## {inventory page name}`, a blank line, then one bullet per field in `templates/repo-contents.md`'s order — 存取庫、盤點頁、HASH、commit sha、盤點時間 — each written `- {欄位名}:{值}` with a full-width colon. **The third argument is the H2 heading, that is the inventory page's actual name — the very page this sub agent just wrote**, matching the entry file's own heading byte for byte; do not derive it from a `REPO_{HASH}` formula, take the page name you actually saved. The `2` is the column number of the old table column that holds the content-page link — the 盤點頁 column — and it is used only for the automatic conversion: while the page on the wiki is still a markdown table, the script takes the last path segment of that column's link URL as the H2 heading, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this repository's existing entry gets appended as a new one, one repository ends up with two blocks, and the old block is never updated again. Its 盤點頁 bullet holds the absolute URL from `gitea.sh wiki-url {REPO repo} REPO_{HASH}`, written as `[{text}]({url})` — take that URL first, check it with `jsc-gitea/tools/link-check.sh`, and branch on both exit codes per "Every link comes from `wiki-url`, and is checked before it is written" below, because the entry must never carry an empty or dead link bullet.
- The `REPO_CONTENTS` read branches by exit code, and only exit 4 opens the create path — see "Contents pages are appended, never overwritten" below. `REPO_{HASH}` is a content page for one repository, so rewriting it whole is correct; the directory page around it is not. - Never hand-edit `REPO_CONTENTS`, and never touch a block belonging to another repository. Branch on the script's exit code — see "Contents pages are appended, never overwritten" below. `REPO_{HASH}` is a content page for one repository, so rewriting it whole is correct; the directory page around it is not.
- For each reuse candidate, confirm the file path and method name first, then analyze whether its logic fits the requirement. Reject a candidate only for a stated reason, and record both the candidate and that reason in the analysis page's 複用決策 field. - For each reuse candidate, confirm the file path and method name first, then analyze whether its logic fits the requirement. Reject a candidate only for a stated reason, and record both the candidate and that reason in the analysis page's 複用決策 field.
Completion condition: every user story has reached consensus under both conditions of `references/consensus.md`, and every reuse decision — reused, or rejected with its reason — is recorded in 複用決策. Completion condition: every user story has reached consensus under both conditions of `references/consensus.md`, and every reuse decision — reused, or rejected with its reason — is recorded in 複用決策.
@@ -38,25 +41,75 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
2. Every scenario states its input, its expected result, its data source and the work package it belongs to. 2. Every scenario states its input, its expected result, its data source and the work package it belongs to.
3. **Every TDD todo is one whole cycle**: one seam, one failing test first, one minimal implementation, one green verification, and a post-green refactor where it is needed. Never split the red test, the minimal implementation and the green verification into separate todos. Seams and anti-patterns: `references/tdd.md`. 3. **Every TDD todo is one whole cycle**: one seam, one failing test first, one minimal implementation, one green verification, and a post-green refactor where it is needed. Never split the red test, the minimal implementation and the green verification into separate todos. Seams and anti-patterns: `references/tdd.md`.
4. Completion condition: every user story has scenarios under `## 使用者故事驗收計畫`; every scenario carries an explicit acceptance method and data source; the scenario count matches the story's complexity; every work package has its own subsection under `## 測試計畫(TDD)`; every implementation work package holds at least one test-first `[ ]` todo; and every todo states its seam, its acceptance scenario, the behaviour the test asserts, the minimal implementation scope and how green is verified. A pure delivery package may use document-verification or sample-data-verification todos instead, and still states the test evidence or the review evidence that proves the spec is usable. 4. Completion condition: every user story has scenarios under `## 使用者故事驗收計畫`; every scenario carries an explicit acceptance method and data source; the scenario count matches the story's complexity; every work package has its own subsection under `## 測試計畫(TDD)`; every implementation work package holds at least one test-first `[ ]` todo; and every todo states its seam, its acceptance scenario, the behaviour the test asserts, the minimal implementation scope and how green is verified. A pure delivery package may use document-verification or sample-data-verification todos instead, and still states the test evidence or the review evidence that proves the spec is usable.
10. **Write the analysis page and its catalogue entries in one wiki pass.** Apply `templates/analyze-page.md` to create or update the analysis page and write it back via `jsc-gitea:wiki`; the page content is Traditional Chinese, exactly as the template dictates. `ANALYZE_CONTENTS` and `PLAN_CONTENTS` are then upserted per "Contents pages are appended, never overwritten" below: read each page back, add this analysis's row to `ANALYZE_CONTENTS` if it is missing and otherwise refresh it, flip only this plan's status in `PLAN_CONTENTS` to the literal 「已分析」, and write each whole page back. Completion condition: the analysis page is saved on the wiki carrying every section the template dictates — the source branch, the head sha and the 未決項 section (「無」 when there is none) included — and, for a new page, `ANALYZE_CONTENTS` shows its new row and `PLAN_CONTENTS` shows the literal 「已分析」, both saved on the wiki. 10. **Write the analysis page and its catalogue entries in one wiki pass.** Apply `templates/analyze-page.md` to create or update the analysis page **in the ANALYZE wiki repo** and write it back via `jsc-gitea:wiki`; the page content is Traditional Chinese, exactly as the template dictates. **Every link the page carries — the plan page, the inventory page, issues, anything external — is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the write**, per "Every link comes from `wiki-url`, and is checked before it is written" below. Both directory entries then go through `jsc-gitea/tools/wiki-contents.sh`, never a hand-edited page. Each entry file holds one H2 block: the heading, a blank line, then one bullet per field in that template's order, each written `- {欄位名}:{值}` with a full-width colon.
11. **Stage report — the last thing this stage does, including every early stop** (the model gate blocked, the working tree did not match `origin/{source-branch}`, no plan was selectable, a wiki read or write failed). Run `tools/stage-report.sh analyze` with one `--page TYPE:{page}` per wiki page this run wrote — `ANALYZE_{HASH}`, `ANALYZE_CONTENTS`, `PLAN_CONTENTS`, and `REPO_{HASH}` plus `REPO_CONTENTS` when a re-inventory happened — plus `--worklog` and `--worklog-heading` when a work log entry exists. No work log yet: write this stage's log content to a file and pass `--pending-file {file} --log-hash {HASH}` so it is held for the next `jsc-log:worklog` run. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it. - `jsc-gitea/tools/wiki-contents.sh upsert ANALYZE 2 {analysis page name} {entry file} templates/analyze-contents.md` — the block follows `templates/analyze-contents.md` (計畫名稱、分析頁、HASH、工作包、未完成項目、狀態) and **the third argument is the H2 heading, that is the analysis page's actual name — the very page this step just saved** (on the live wiki they look like `ANALYZE_20260821_100552_104F0709`, a timestamp plus 8 characters, so never build the key from an `ANALYZE_{HASH}` formula). The `2` is the column number of the old table column that holds the content-page link — the 分析頁 column — and it is used only for the automatic conversion: while the page is still a markdown table, the script takes the last path segment of that column's link URL as the H2 heading, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this analysis's existing entry gets appended as a new one, one analysis ends up with two blocks, and the old block is never updated again. Its 分析頁 bullet holds the absolute URL from `gitea.sh wiki-url {ANALYZE repo} ANALYZE_{HASH}`, written as `[{text}]({url})`: take that URL after the analysis page is saved, check it with `jsc-gitea/tools/link-check.sh`, and branch on both exit codes per "Every link comes from `wiki-url`, and is checked before it is written" below, because the entry must never carry an empty or dead link bullet.
- `jsc-gitea/tools/wiki-contents.sh upsert PLAN 2 {plan page name} {entry file} templates/plan-contents.md` — rebuild that plan's block from the block `PLAN_CONTENTS` already holds, change only the `- 狀態:` bullet to the literal 「已分析」, and keep every other bullet (including the absolute plan-page link) byte-for-byte as it was. The third argument is again the H2 heading: the plan page's actual name, copied from the heading of the block step 2 read off the page, never rebuilt from a `PLAN_{HASH}` formula. The `2` is the column number of the old table column that holds the content-page link — the 計畫頁 column — used only for the automatic table conversion, on the same terms and with the same duplicate-block consequence as the `ANALYZE` call above.
Both runs branch on the exit code per "Contents pages are appended, never overwritten" below. Completion condition: the analysis page is saved on the wiki carrying every section the template dictates — the source branch, the head sha and the 未決項 section (「無」 when there is none) included — every `wiki-url` call this step made returned 0 and its URL is the one in the entry, every link written by this step was cleared by a `link-check.sh` run that exited 0, both `wiki-contents.sh` runs exited 0, and `ANALYZE_CONTENTS` shows this analysis's `## ANALYZE_{HASH}` block while the `## PLAN_{HASH}` block shows the literal 「已分析」.
11. **Stage report — the last thing this stage does, including every early stop** (the model gate blocked, the working tree did not match `origin/{source-branch}`, no plan was selectable, a wiki read or write failed). Run `tools/stage-report.sh analyze` with one `--page TYPE:{page}` per wiki page this run wrote — `--page ANALYZE:ANALYZE_{HASH}`, `--page CONTENTS:ANALYZE_CONTENTS`, `--page CONTENTS:PLAN_CONTENTS`, and `--page REPO:REPO_{HASH}` plus `--page CONTENTS:REPO_CONTENTS` when a re-inventory happened. **Every directory page takes the `CONTENTS` type**: the script resolves each page's repo from the TYPE you pass, and a directory page passed under its old type resolves the wrong repo and prints no URL. Add `--worklog` and `--worklog-heading` when a work log entry exists. No work log yet: write this stage's log content to a file — with a Bash heredoc or `mktemp` per Hard limits, never with `Write` or `Edit` — and pass `--pending-file {file} --log-hash {HASH}` so it is held for the next `jsc-log:worklog` run. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it.
12. **Write this run's `skill-end` status event — the very last thing this stage does, right after step 11, on every path including every early stop.** Run `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:analyze {status} {exit} [detail]`, naming the script the way this stage already names `jsc-hooks/hooks/sdlc-gate.sh` in step 1. The matching `skill-start` event is written by jsc-hooks on its own, so this step owes only the `end`: a hook fires on the skill tool call and this stage's work happens in the model turns after it, so **no hook can see how this run ended**. A `start` with no `end` is what an aborted run looks like in the record, and this step is the only thing that keeps a finished run from looking like one.
`{status}` is one of five words, never a sixth:
| Status | When `analyze` reports it |
| --- | --- |
| `ok` | Every step's completion condition is met: the gate passed, the source branch was confirmed and `origin/{source-branch}` matched HEAD, every user story reached consensus, the WBS, the CPM figures and the TDD todos are on the page, `ANALYZE_{HASH}` is saved, every directory entry block this run owed was upserted, and `tools/stage-report.sh` exited 0 |
| `blocked` | A check that lives in code stopped the run before any analysis started: `sdlc-gate.sh lock analyze` exited non-zero because the model carries no `reasoning-max` tag, or step 4.3 found HEAD not pointing at the same commit as `origin/{source-branch}`. Nothing was analyzed, so this is **never `failed`** — both are the guard working, and recording either as a failure sends the next reader hunting for a defect that is not there |
| `failed` | The run got past those checks and then a write did not land: the `ANALYZE_{HASH}` or `REPO_{HASH}` write failed, `wiki-url` returned 5, 7 or 8, `link-check.sh` returned 1 so nothing was written, or a `wiki-contents.sh` run returned 1, 7 or 8 |
| `degraded` | The analysis page is saved but not every directory followed it: `ANALYZE_CONTENTS` was upserted while `PLAN_CONTENTS` still shows 「未分析」, a re-inventory wrote `REPO_{HASH}` but not its `REPO_CONTENTS` block, `wiki-contents.sh` returned 3, or `tools/stage-report.sh` exited 1. The analysis exists; what is missing is a directory entry that lets anyone find it |
| `aborted` | The user stopped the run, or the run stopped itself because its premise did not hold — step 2 found both directory pages empty, so there was nothing to analyze |
`{exit}` is the exit code of the script whose verdict decided the status — the gate's code for `blocked`, the failing script's code for `failed` and `degraded` — and `0` when nothing exited non-zero, `ok` and `aborted` included. `[detail]` is optional and Traditional Chinese per the STE100 rule: one line, no line break, naming what decided the status (for example 「工作目錄與來源分支不一致」 or 「計畫目錄頁狀態未改」). The script truncates it at 200 characters, so put the short reason there and nothing else.
**A failure in this step never changes this stage's verdict.** The script is not found (jsc-hooks is not installed on this machine, or this CLI's layout puts it somewhere else) → skip the event quietly and carry on; nothing is reported to the user and no step is re-run. The three recording sub-commands are built to exit 0 even when the write fails, so a non-zero code here means only that the call itself was malformed (exit 2, a usage error) — fix the arguments once and, either way, never turn a finished stage into a failed one because the record of it failed. Completion condition: one `skill-end` event has been written for this run, or the script could not be found and that skip is the reason no event exists.
## Contents pages are appended, never overwritten ## Contents pages are appended, never overwritten
`ANALYZE_CONTENTS`, `PLAN_CONTENTS` and `REPO_CONTENTS` are shared directories: every row on them belongs to somebody's plan, analysis or repository, and this run reads none of those rows from anywhere else. So every write to them is an upsert of one row on top of the content just read — add the row if missing, otherwise refresh it, then `wiki-put` the whole page. Whole-page overwrite is forbidden, and a row this run does not own stays untouched. `ANALYZE_CONTENTS`, `PLAN_CONTENTS` and `REPO_CONTENTS` are shared directories in the CONTENTS wiki repo: every H2 block on them belongs to somebody's plan, analysis or repository, and this run reads none of those blocks from anywhere else. So every write to them is an upsert of one block — never a whole-page overwrite, and never a block this run does not own. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, converts a page still holding a markdown table into blocks first, replaces the block whose H2 heading matches the key and appends at the end when none matches, then writes the page back.
That rests entirely on reading the old page back, so branch the `wiki-get` on its exit code: Branch on its exit code:
| Exit | What this step does | | Exit | What this step does |
| --- | --- | | --- | --- |
| 0 | the page is there — upsert this run's row into the content that came back, then write the whole page | | 0 | the block is in place — carry the `updated` or `added` word it printed into the stage report |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template | | 1 | the page content could not be assembled, or the write failed — report it as a failed write and go to step 11 as a failure. **A page holding no matching block is not this code**: with nothing to replace the script appends the block and exits 0 |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing | | 2 | an argument was rejected (unknown type, bad key-column number, missing entry file) — fix the argument and run it again; nothing was written |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged | | 3 | no CONTENTS wiki repo is configured — stop and report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set. The content page this run wrote is saved and stays saved |
| 4 | the directory page is absent and no template was passed. **Every call in this stage already passes that page's template, so this code does not come out of this skill's call** — a wrong template path is rejected as 2, not as 4. Seeing it anyway means the template file is not where the plugin puts it: confirm the plugin installation is complete and run it again. Never answer it by dropping the template argument |
| 7 | the key is invalid or lacks permission — stop and report the key problem; the script wrote nothing, which is what keeps every other block alive |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" makes this step write a fresh template over a live directory, and every other row is gone — the write carries no merge and no backup. Content pages (`ANALYZE_{HASH}`, `PLAN_{HASH}`, `REPO_{HASH}`) are the opposite case: each belongs to one subject, so rewriting one whole is correct. The distinction is the page, not the write. Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" would write a fresh template over a live directory, and every other block is gone — the write carries no merge and no backup. Content pages (`ANALYZE_{HASH}`, `PLAN_{HASH}`, `REPO_{HASH}`) are the opposite case: each belongs to one subject and lives in its own type's repo, so rewriting one whole is correct. The distinction is the page, not the write.
Completion condition: every contents-page write this stage made names the `wiki-get` exit code it branched on, and no page was created on any code other than 4. Completion condition: every directory-page write this stage made names the `wiki-contents.sh` exit code it branched on, and no directory page was created on any code other than 4.
## Every link comes from `wiki-url`, and is checked before it is written
**One syntax.** Every link this stage writes — on `ANALYZE_{HASH}` and `REPO_{HASH}`, in the `ANALYZE_CONTENTS`, `PLAN_CONTENTS` and `REPO_CONTENTS` entry blocks, in the stage report — is written as `[{text}]({url})`. The `{url}` is the absolute URL `gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
Run `wiki-url` **after** the content page it names is saved, and branch on its exit code; this is the same branching `jsc-log:worklog` runs over the same call.
| Exit | What this step does |
| --- | --- |
| 0 | use the URL it printed, verbatim |
| 4 | the page is not on the wiki, so the write that was supposed to create it has not landed — go back to that write (step 5.2 for `REPO_{HASH}`, step 10 for `ANALYZE_{HASH}`) and come here again only once the page is saved |
| 5 | the page exists but carries no `html_url` — stop and report it, and never assemble the URL by hand; a hand-built path is not the one Gitea serves |
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4: the content page is alive, and treating it as absent records a live page as one that was never written |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL. An entry whose link bullet is empty is a directory entry that points nowhere, the reader has no way to reach the page it names, and the next run replaces that block as if it were correct.
**Checked before it is written.** Collect every link the page or the entry block is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this step does |
| --- | --- |
| 0 | every link answers — write the page or upsert the entry block |
| 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 2 | usage error: not one URL was passed — pass the links and run it again |
| 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and a block rewritten on that reading loses the links that were fine |
Completion condition: every entry block this stage upserted carries a URL that came out of a `wiki-url` run that exited 0, every page and block this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code from either script was branched on as these tables say.
## Delivery package is WP-01 ## Delivery package is WP-01
@@ -76,4 +129,5 @@ Every sample value on the analysis page — request and response payloads, field
- Never output a code snippet (file paths and method names are allowed). - Never output a code snippet (file paths and method names are allowed).
- **Never modify any file in the working directory.** This limit is enforced in code where the CLI allows it: `jsc-hooks/hooks/write-guard.sh` in `stage` mode runs as a `PreToolUse` hook and blocks `Write`, `Edit` and `MultiEdit` while this stage's lock exists — the same lock state `jsc-hooks/hooks/sdlc-gate.sh lock analyze` writes in step 1. **Only claude has `PreToolUse`.** Codex, copilot, antigravity and kiro never reach that hook, so on those four CLIs this line is the only thing holding the limit. - **Never modify any file in the working directory.** This limit is enforced in code where the CLI allows it: `jsc-hooks/hooks/write-guard.sh` in `stage` mode runs as a `PreToolUse` hook and blocks `Write`, `Edit` and `MultiEdit` while this stage's lock exists — the same lock state `jsc-hooks/hooks/sdlc-gate.sh lock analyze` writes in step 1. **Only claude has `PreToolUse`.** Codex, copilot, antigravity and kiro never reach that hook, so on those four CLIs this line is the only thing holding the limit.
- **Every entry file this stage builds — `REPO_CONTENTS` in step 5.2, `ANALYZE_CONTENTS` and `PLAN_CONTENTS` in step 10 — and the pending-file of step 11 are produced with a Bash heredoc or `mktemp`, in a temporary directory, never with the `Write` or `Edit` tool.** That gate blocks on the stage lock alone and never looks at the path, so a `Write` of any of those four files is blocked by this stage's own lock and the stage cannot finish: no directory entry gets written and this stage's log content is never parked. All four are scratch input to a script, they live outside the working directory, and writing them this way keeps the limit above intact — nothing in the working directory is touched.
- Never switch, create or clean branches in this stage; ask the user to do it. - Never switch, create or clean branches in this stage; ask the user to do it.
+69 -20
View File
@@ -1,18 +1,23 @@
--- ---
name: implement name: implement
description: SDLC implementation stage. Gate on capability tags enforced in code by sdlc-gate (implement requires coding), settle every open work package's PR comments first, then claim a ready package before confirming the analysis page's source branch - it is both the worktree base and the PR target. Every already-open work package's PR comments get triaged via tools/wp-gate.sh check and fixed by sub agents only after jsc-ask consensus, never blocking an unrelated package; every candidate is gated in code by tools/wp-gate.sh check-deps before it reaches the options, and claiming records the package number via tools/wp-gate.sh claim so tools/wp-gate.sh owns keeps every session on its own package's PR. Claim a ready work package from ANALYZE_CONTENTS with a work ticket, confirm a delivery package's content type, then complete its TDD todos one at a time inside a worktree built from origin/{source-branch}, updating the wiki after every item, closing with the two side-by-side audits jsc-review code-review and jsc-review api-doc (the latter gated by swagger-detect.sh and explicitly skipped where the project has no Swagger support), one PR back to the source branch, a jsc-gitea pr-watch.sh poll that holds until that PR merges, a jsc-log:worklog entry per finished task, the chosen delivery document, an optional MAINTAIN_CONTENTS entry, and a tools/stage-report.sh report covering the model tag verdict, the worklog link, every wiki link written, the worktree and the three branches. Use when analysis is done and code must be written; not for planning or analysis. description: SDLC implementation stage. Gate on capability tags enforced in code by sdlc-gate (implement requires coding), settle every open work package's PR comments first, then claim a ready package before confirming the analysis page's source branch - it is both the worktree base and the PR target. Every already-open work package's PR comments get triaged via tools/wp-gate.sh check and fixed by sub agents only after jsc-ask consensus, never blocking an unrelated package; every candidate is gated in code by tools/wp-gate.sh check-deps before it reaches the options, and claiming records the package number via tools/wp-gate.sh claim so tools/wp-gate.sh owns keeps every session on its own package's PR. Claim a ready work package from ANALYZE_CONTENTS with a work ticket, confirm a delivery package's content type, then complete its TDD todos one at a time inside a worktree built from origin/{source-branch}, updating the wiki after every item, closing with the two side-by-side audits jsc-review code-review and jsc-review api-doc (the latter gated by swagger-detect.sh and explicitly skipped where the project has no Swagger support), one PR back to the source branch, a jsc-gitea pr-watch.sh poll that holds until that PR merges, a jsc-log:worklog entry per finished task, the chosen delivery document, an optional MAINTAIN_CONTENTS entry - every directory H2 block upserted through jsc-gitea/tools/wiki-contents.sh into the separate CONTENTS wiki repo and linked by absolute wiki-url - and a tools/stage-report.sh report covering the model tag verdict, the worklog link, every wiki link written, the worktree and the three branches. Use when analysis is done and code must be written; not for planning or analysis.
--- ---
# implement # implement
Goal: complete the analysis page's todos one by one; **update the wiki status immediately after every completed item**. Goal: complete the analysis page's todos one by one; **update the wiki status immediately after every completed item**.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Step 13 still runs after such a stop.
`jsc-gitea/tools/hash-id` prints the **full 40-character uppercase SHA-1** of its input — no truncation to 8 characters, no prefix rewrite. Every `{HASH}` this stage builds carries that full length: the page names, the worktree path and the work ticket of step 6 alike. Never shorten one by hand.
**Content pages and directory pages live in different wiki repos.** `ANALYZE_{HASH}` sits in the repo `jsc-gitea/tools/gitea.sh wiki-repo ANALYZE` resolves and `DELIVER_{HASH}` in the one `gitea.sh wiki-repo DELIVER` resolves, while the directory pages `ANALYZE_CONTENTS`, `DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` all sit in the repo `gitea.sh wiki-repo CONTENTS` resolves: `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to the page type's own variable. **All three are bullet-list directory pages, not tables**: one H2 block per entry, the heading being that entry's key — for `ANALYZE_CONTENTS` and `DELIVER_CONTENTS` it is the content page's actual name, the page the run itself just wrote, never a `{TYPE}_{HASH}` formula; for `MAINTAIN_CONTENTS` it is the repository's own `{owner}/{repo}`, because `MAINTAIN` has no content page and a page-shaped key there would name a page that does not exist — and the fields a one-level bullet list under it, each written `- {欄位名}:{值}` in the order that page's template gives. Every directory entry links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`, written as `[{text}]({url})` — one link syntax, whichever wiki the two pages sit in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Steps 13 and 14 still run after such a stop.
## Steps ## Steps
1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock implement`. This stage requires the `coding` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict. 1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock implement`. This stage requires the `coding` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict.
2. **Read the analysis pages, then settle every already-open work-package PR's comments — this keeps existing PRs moving and can clear a dependency for step 4, but it does not by itself decide which new package may start (step 4 does)**: 2. **Read the analysis pages, then settle every already-open work-package PR's comments — this keeps existing PRs moving and can clear a dependency for step 4, but it does not by itself decide which new package may start (step 4 does)**:
1. Read `ANALYZE_CONTENTS`, then read every analysis page it lists as unfinished. **Keep this read: steps 3, 5 and 8 reuse it and never read the same pages again.** Completion condition: for every unfinished analysis page you hold its WBS table, its PR column, its source branch and the repositories it names. 1. Read `ANALYZE_CONTENTS` out of the CONTENTS wiki repo (`gitea.sh wiki-repo CONTENTS`, never the ANALYZE one), then read every analysis page it lists as unfinished out of the ANALYZE wiki repo. **Read the directory page as H2 blocks, not table rows**: each `## ANALYZE_{HASH}` heading is one analysis and is that analysis page's name, its 計畫名稱、分析頁、HASH、工作包、未完成項目、狀態 are the bullets under it, and an analysis counts as unfinished when its `- 狀態:` bullet reads 「未完成」. The WBS table, the PR column and the todos are on the `ANALYZE_{HASH}` content page itself, which keeps its table layout. **Keep this read: steps 3, 5 and 8 reuse it and never read the same pages again.** Completion condition: for every unfinished analysis page you hold its WBS table, its PR column, its source branch and the repositories it names.
2. **Prefetch every open PR's state in one batch.** For every work package holding a PR that is not marked merged, run `jsc-sdlc/tools/wp-gate.sh check {owner}/{repo} {index} --since {the comment timestamp recorded in that PR column}`. Drop `--since` when that package has no recorded timestamp yet. **These calls do not depend on each other — run them concurrently and collect every result before you ask the user anything.** The consensus rounds and the fixes that follow stay one comment at a time. Completion condition: every open PR has a recorded exit code and `status=` line. 2. **Prefetch every open PR's state in one batch.** For every work package holding a PR that is not marked merged, run `jsc-sdlc/tools/wp-gate.sh check {owner}/{repo} {index} --since {the comment timestamp recorded in that PR column}`. Drop `--since` when that package has no recorded timestamp yet. **These calls do not depend on each other — run them concurrently and collect every result before you ask the user anything.** The consensus rounds and the fixes that follow stay one comment at a time. Completion condition: every open PR has a recorded exit code and `status=` line.
3. Exit 0 (`status=merged`) clears that package: remove its worktree (`references/branch.md`) and mark the package done on the analysis page. 3. Exit 0 (`status=merged`) clears that package: remove its worktree (`references/branch.md`) and mark the package done on the analysis page.
4. Exit 1 (`status=open` or `status=closed-unmerged`) means that package's own PR is not settled yet. **This does not block picking a different, unrelated work package** — SDLC implementation can run several independent packages in parallel; an unmerged PR only holds back packages that depend on it (step 4 checks that specifically), never the whole analysis page. Sub-steps 2.5 to 2.10 are the one comment round this skill owns; step 11.5 runs the same sub-steps for the PR it just opened. 4. Exit 1 (`status=open` or `status=closed-unmerged`) means that package's own PR is not settled yet. **This does not block picking a different, unrelated work package** — SDLC implementation can run several independent packages in parallel; an unmerged PR only holds back packages that depend on it (step 4 checks that specifically), never the whole analysis page. Sub-steps 2.5 to 2.10 are the one comment round this skill owns; step 11.5 runs the same sub-steps for the PR it just opened.
@@ -25,7 +30,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
11. Exit 2 (`status=usage`) means bad arguments — a malformed `{owner}/{repo}`, a missing index, or a `--since` value that is not the previous round's `latest=`. Fix the arguments and run it again. Completion condition: the rerun returned 0, 1 or 3; a usage error never counts as merged, settled or blocked. 11. Exit 2 (`status=usage`) means bad arguments — a malformed `{owner}/{repo}`, a missing index, or a `--since` value that is not the previous round's `latest=`. Fix the arguments and run it again. Completion condition: the rerun returned 0, 1 or 3; a usage error never counts as merged, settled or blocked.
12. Exit 3 (`status=missing-dep`) means the gate could not decide (a missing dependency script, or the PR could not be found). Report it and stop — an undecidable gate never counts as merged. 12. Exit 3 (`status=missing-dep`) means the gate could not decide (a missing dependency script, or the PR could not be found). Report it and stop — an undecidable gate never counts as merged.
13. Completion condition: every work package holding an unmerged PR has had this run's comments triaged (fixed, no fix needed, or cannot fix), logged and reported; a package left unmerged after this does not block steps 3 and 4 for packages that do not depend on it. 13. Completion condition: every work package holding an unmerged PR has had this run's comments triaged (fixed, no fix needed, or cannot fix), logged and reported; a package left unmerged after this does not block steps 3 and 4 for packages that do not depend on it.
3. From the pages read in step 2.1, list what is unfinished: plan name, HASH, work package number, count of open items. A selectable work package satisfies both: **unfinished, and not holding a work ticket**. Dependencies are not judged here and never by eyeballing the 相依 column — step 4's `wp-gate.sh check-deps` is the only judge. Completion condition: you have listed every selectable work package, or reported that none is selectable and stopped. 3. From the pages read in step 2.1, list what is unfinished: plan name, HASH, work package number, count of open items. **The plan name and the HASH come from the directory page's H2 block — the heading gives the analysis page name and its HASH, the `- 計畫名稱:` bullet gives the plan name — and the work package number and its open items come from that analysis page's own WBS and todo tables.** A selectable work package satisfies both: **unfinished, and not holding a work ticket**. Dependencies are not judged here and never by eyeballing the 相依 column — step 4's `wp-gate.sh check-deps` is the only judge. Completion condition: you have listed every selectable work package, or reported that none is selectable and stopped.
4. **Every candidate passes the dependency gate before the user sees it — the gate lives in code, not in this text, and it judges one candidate at a time, never every open PR on the page**: 4. **Every candidate passes the dependency gate before the user sees it — the gate lives in code, not in this text, and it judges one candidate at a time, never every open PR on the page**:
1. Run `jsc-sdlc/tools/wp-gate.sh check-deps {owner}/{repo} {wp-number} --analyze ANALYZE_{HASH}` **once per candidate from step 3, before asking the user anything**. Candidates do not depend on each other's verdict, so run these concurrently. The script re-reads the caller-specified analysis page and queries Gitea itself — it does not trust anything you already read or concluded, and it never guesses the page from the repository hash. 1. Run `jsc-sdlc/tools/wp-gate.sh check-deps {owner}/{repo} {wp-number} --analyze ANALYZE_{HASH}` **once per candidate from step 3, before asking the user anything**. Candidates do not depend on each other's verdict, so run these concurrently. The script re-reads the caller-specified analysis page and queries Gitea itself — it does not trust anything you already read or concluded, and it never guesses the page from the repository hash.
2. Branch on each candidate's exit code. Exit 0 (`status=ready`) → put it in the option list. Exit 1 (`status=blocked`) → keep it out of the option list, and report which dependency work package's PR is not merged yet. Exit 3 (`status=missing-dep`) is an undecidable gate — report it and stop; never treat an undecidable result as either ready or blocked. Exit 2 (`status=usage`) → bad arguments or a missing `--analyze`; fix them and run that candidate again, and leave it out of the options until the rerun returns a verdict. Completion condition: every candidate from step 3 carries one of these verdicts, and only the `ready` ones remain. 2. Branch on each candidate's exit code. Exit 0 (`status=ready`) → put it in the option list. Exit 1 (`status=blocked`) → keep it out of the option list, and report which dependency work package's PR is not merged yet. Exit 3 (`status=missing-dep`) is an undecidable gate — report it and stop; never treat an undecidable result as either ready or blocked. Exit 2 (`status=usage`) → bad arguments or a missing `--analyze`; fix them and run that candidate again, and leave it out of the options until the rerun returns a verdict. Completion condition: every candidate from step 3 carries one of these verdicts, and only the `ready` ones remain.
@@ -37,7 +42,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
2. **Show the recorded value and take a single-key confirmation.** The analysis page already carries the answer, so this is a confirmation, not a fresh question: print `origin/{source-branch}` as both the worktree base and the PR target for this work package, and accept one key to confirm it. Two cases have no shortcut and go through the full `jsc-ask:ask` decision tree, each option stating its impact scope: **the analysis page records no source branch**, and **`origin/{source-branch}` does not exist on the remote**. 2. **Show the recorded value and take a single-key confirmation.** The analysis page already carries the answer, so this is a confirmation, not a fresh question: print `origin/{source-branch}` as both the worktree base and the PR target for this work package, and accept one key to confirm it. Two cases have no shortcut and go through the full `jsc-ask:ask` decision tree, each option stating its impact scope: **the analysis page records no source branch**, and **`origin/{source-branch}` does not exist on the remote**.
3. **A source branch missing from the remote is a stop-and-report condition, never a silent fallback.** That rule (section 「來源分支在遠端找不到」), the remote-only basis and the uncommitted-changes rules: `references/branch.md`. 3. **A source branch missing from the remote is a stop-and-report condition, never a silent fallback.** That rule (section 「來源分支在遠端找不到」), the remote-only basis and the uncommitted-changes rules: `references/branch.md`.
4. Completion condition: the user has confirmed the source branch — by the single key, or through the decision tree in either exception case — and it is recorded in the analysis page's 「來源分支」 column. 4. Completion condition: the user has confirmed the source branch — by the single key, or through the decision tree in either exception case — and it is recorded in the analysis page's 「來源分支」 column.
6. **Generate a work ticket and claim the package on the page**: format `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}`. `{HASH}` = the shared wiki hash for `{owner}/{repo}`, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). Rename the current session to the ticket name; skip the rename only when the CLI exposes no rename command. Write the ticket into the picked work package's ticket column (the zh-TW field 「工作證」) on the analysis page and save it back to the wiki. Completion condition: the ticket string exists, it is saved in that package's 「工作證」 column on the wiki, and you have reported it together with which branch applied — renamed, or skipped because this CLI has no rename command. 6. **Generate a work ticket and claim the package on the page**: format `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}`. `{HASH}` = the shared wiki hash for `{owner}/{repo}`, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`), so the ticket carries the same full 40 characters the page names do. The ticket is not a wiki page and no page-name rule applies to it, but it is still passed on whole: hand it to the session rename and write it into the column exactly as `hash-id` printed it. Rename the current session to the ticket name; skip the rename only when the CLI exposes no rename command. Write the ticket into the picked work package's ticket column (the zh-TW field 「工作證」) on the analysis page and save it back to the wiki. Completion condition: the ticket string exists, it is saved in that package's 「工作證」 column on the wiki, and you have reported it together with which branch applied — renamed, or skipped because this CLI has no rename command.
7. **A delivery/handover package confirms its content before its first todo**: 7. **A delivery/handover package confirms its content before its first todo**:
1. Ask per `jsc-ask:ask` rules what this delivery must contain. The options are fixed: **1. API 文件** and **2. 由使用者輸入**. State the impact scope on each. **Never assume the type, and never skip this — the answer decides what the whole package produces.** 1. Ask per `jsc-ask:ask` rules what this delivery must contain. The options are fixed: **1. API 文件** and **2. 由使用者輸入**. State the impact scope on each. **Never assume the type, and never skip this — the answer decides what the whole package produces.**
2. Required fields, sample-data order and the new-versus-existing parameter marking: `references/deliver-formats.md`. 2. Required fields, sample-data order and the new-versus-existing parameter marking: `references/deliver-formats.md`.
@@ -59,7 +64,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
3. **Start both audits together and let them run side by side** — they read the same diff and neither one's verdict changes the other's input, so the closing wait costs one audit, not two. Completion condition: both audits have returned, and both cleared — a pass from `code-review`, and either a pass or a reported skip from the API document audit. 3. **Start both audits together and let them run side by side** — they read the same diff and neither one's verdict changes the other's input, so the closing wait costs one audit, not two. Completion condition: both audits have returned, and both cleared — a pass from `code-review`, and either a pass or a reported skip from the API document audit.
11. **One work package finished → commit, push, PR back to the source branch, then hold on that PR until it merges**: 11. **One work package finished → commit, push, PR back to the source branch, then hold on that PR until it merges**:
1. Call `jsc-git:pr` from inside the worktree, **passing `{source-branch}` as the base branch**. One package, one PR; the branch ladder and how the base is derived are in `references/branch.md`. 1. Call `jsc-git:pr` from inside the worktree, **passing `{source-branch}` as the base branch**. One package, one PR; the branch ladder and how the base is derived are in `references/branch.md`.
2. Write the PR URL and number into that work package's PR column on the analysis page and save it back to the wiki, so the next run of this skill can find it (step 2). 2. Write the PR URL and number into that work package's PR column on the analysis page as `[{text}]({url})`, **checked with `jsc-gitea/tools/link-check.sh` before the save** — see "Every link is checked before it reaches a page" below — and save the page back to the wiki, so the next run of this skill can find it (step 2).
3. Run `jsc-sdlc/tools/wp-gate.sh lock {owner}/{repo} {index} --wp {wp-number}`. The lock is what makes step 2's gate hold across work sessions — a new session starts blocked until that PR merges — and `--wp` is what hangs this PR on the claim from step 4.5, so `owns` can keep other sessions off it. Leave `--wp` out and every session is free to touch this PR. Branch on the exit code: exit 0 (`status=locked`) → proceed. Exit 2 (`status=usage`) → fix the arguments and run it again. Exit 3 (`status=missing-dep`) → the lock could not be recorded; report it and stop, because without the lock the next session starts unblocked on a PR that has not merged. Completion condition: the script printed `status=locked` and named the work package. 3. Run `jsc-sdlc/tools/wp-gate.sh lock {owner}/{repo} {index} --wp {wp-number}`. The lock is what makes step 2's gate hold across work sessions — a new session starts blocked until that PR merges — and `--wp` is what hangs this PR on the claim from step 4.5, so `owns` can keep other sessions off it. Leave `--wp` out and every session is free to touch this PR. Branch on the exit code: exit 0 (`status=locked`) → proceed. Exit 2 (`status=usage`) → fix the arguments and run it again. Exit 3 (`status=missing-dep`) → the lock could not be recorded; report it and stop, because without the lock the next session starts unblocked on a PR that has not merged. Completion condition: the script printed `status=locked` and named the work package.
4. **The work package is finished the moment its PR is open — call `jsc-log:worklog` now**, under the Rules section's one-task-one-entry rule, reusing the PLAN and ANALYZE wiki repositories and page URLs resolved for this package in step 2.10. Completion condition: the entry is saved on `LOG_{HASH}` before the wait starts. 4. **The work package is finished the moment its PR is open — call `jsc-log:worklog` now**, under the Rules section's one-task-one-entry rule, reusing the PLAN and ANALYZE wiki repositories and page URLs resolved for this package in step 2.10. Completion condition: the entry is saved on `LOG_{HASH}` before the wait starts.
5. **Wait for the merge with `jsc-gitea/tools/pr-watch.sh {owner}/{repo} {index}`** — it polls every 60 seconds (`JSC_PR_WATCH_INTERVAL` overrides), never times out, and never repeats a comment it already reported. Branch on its exit code: 5. **Wait for the merge with `jsc-gitea/tools/pr-watch.sh {owner}/{repo} {index}`** — it polls every 60 seconds (`JSC_PR_WATCH_INTERVAL` overrides), never times out, and never repeats a comment it already reported. Branch on its exit code:
@@ -72,33 +77,77 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
12. **Deliver the document, then register for maintenance** — one closing pass over the two questions this stage owes the user. A finished work package is a delivery, so **always ask before producing it; never pick a format silently and never skip either question**: 12. **Deliver the document, then register for maintenance** — one closing pass over the two questions this stage owes the user. A finished work package is a delivery, so **always ask before producing it; never pick a format silently and never skip either question**:
1. Ask per `jsc-ask:ask` rules which delivery format to produce. The options are fixed: **a `DELIVER_{HASH}` wiki page** or **a Gitea issue comment**. State the impact scope on each (the wiki page lives beside the plan and analysis pages; the issue comment reaches whoever follows that issue). 1. Ask per `jsc-ask:ask` rules which delivery format to produce. The options are fixed: **a `DELIVER_{HASH}` wiki page** or **a Gitea issue comment**. State the impact scope on each (the wiki page lives beside the plan and analysis pages; the issue comment reaches whoever follows that issue).
2. Both formats use the same structure — `templates/deliver-page.md`, in Traditional Chinese. Only the destination differs. Sample values and personal-data handling: `references/deliver-formats.md`. 2. Both formats use the same structure — `templates/deliver-page.md`, in Traditional Chinese. Only the destination differs. Sample values and personal-data handling: `references/deliver-formats.md`.
3. Wiki page: write `DELIVER_{HASH}` through `jsc-gitea:wiki`, where `{HASH}` comes from `jsc-gitea/tools/hash-id` over `{owner}/{repo}` plus the work package number (for example `plugins/sdlc#WP-01`), so each work package gets its own page instead of overwriting the previous one. Then upsert this work package's row in `DELIVER_CONTENTS` per `templates/deliver-contents.md` — add the row if missing, otherwise refresh it — following "Contents pages are appended, never overwritten" below. 3. Wiki page: write `DELIVER_{HASH}` into the DELIVER wiki repo through `jsc-gitea:wiki` — **every link that page carries is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the write**, per "Every link is checked before it reaches a page" below — where `{HASH}` comes from `jsc-gitea/tools/hash-id` over `{owner}/{repo}` plus the work package number (for example `plugins/sdlc#WP-01`), so each work package gets its own page instead of overwriting the previous one. That input is what keeps the pages apart, and the full 40 characters `hash-id` prints are what the page is named — hashing the repository alone, or shortening the result, puts two work packages on one page again. Then upsert this work package's H2 entry block with `jsc-gitea/tools/wiki-contents.sh upsert DELIVER 5 {delivery page name} {entry file} templates/deliver-contents.md`. The entry file holds the heading `## {delivery page name}`, a blank line, then one bullet per field in `templates/deliver-contents.md`'s order — 計畫名稱、工作包、交付、交付型別、交付頁、HASH、存取庫、交付時間 — each written `- {欄位名}:{值}` with a full-width colon. **The third argument is the H2 heading, that is the delivery page's actual name — the very page this step just wrote**, matching the entry file's own heading byte for byte; take the name you actually saved rather than rebuilding it from a `DELIVER_{HASH}` formula. The `5` is the column number of the old table column that holds the content-page link — the 交付頁 column — and it is used only for the automatic conversion: while the page on the wiki is still a markdown table, the script takes the last path segment of that column's link URL as the H2 heading, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this work package's existing entry gets appended as a new one, one delivery ends up with two blocks, and the old block is never updated again. Its 交付頁 bullet holds the absolute URL from `gitea.sh wiki-url {DELIVER repo} DELIVER_{HASH}`, written as `[{text}]({url})` — run that **after** the delivery page is saved and branch on its exit code, the same branching `jsc-log:worklog` runs over the same call:
4. Issue comment: confirm the issue number with the user (propose the one referenced by the work package or the PR; never guess), then post via `jsc-gitea/tools/gitea.sh api POST /repos/{owner}/{repo}/issues/{n}/comments` with the body passed in as a UTF-8 file — real newlines, never a literal `\n`.
5. With the delivery produced, ask per `jsc-ask:ask` rules whether to register this project for maintenance: upsert this repository's row in `MAINTAIN_CONTENTS` with `templates/maintain-contents.md` — add the row if missing, otherwise refresh it — following "Contents pages are appended, never overwritten" below. Required: repository `{owner}/{repo}`, maintenance method, start date. Optional: end date (NULL = maintain forever), last-maintained time. | Exit | What this step does |
6. Completion condition: the chosen delivery format has actually been produced and you have reported where it landed (wiki page name, or the comment URL), **and** the user has answered the maintenance question with a chosen registration saved on the wiki. | --- | --- |
| 0 | use the URL it printed, verbatim |
| 4 | the page is not on the wiki, so the `DELIVER_{HASH}` write of this sub-step has not landed — write that page first and come here again only once it is saved |
| 5 | the page exists but carries no `html_url` — stop and report it, and never assemble the URL by hand; a hand-built path is not the one Gitea serves |
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4: the delivery page is alive, and treating it as absent records a delivered work package as one that was never delivered |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL: an entry whose 交付頁 bullet is empty names a delivery nobody can open, and the next run replaces that block as if it were correct. **Check that URL with `jsc-gitea/tools/link-check.sh` and build the entry only on exit 0** — a link that does not answer never goes into a directory everyone else reads. Branch on the upsert's own exit code per "Contents pages are appended, never overwritten" below, and never hand-edit the directory page.
4. Issue comment: confirm the issue number with the user (propose the one referenced by the work package or the PR; never guess), then post via `jsc-gitea/tools/gitea.sh api POST /repos/{owner}/{repo}/issues/{n}/comments` with the body passed in as a UTF-8 file — real newlines, never a literal `\n`. Every link in that body is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the post: a comment is as hard to correct as a page once other people have read it.
5. With the delivery produced, ask per `jsc-ask:ask` rules whether to register this project for maintenance — any link that entry carries is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the upsert: upsert this repository's H2 entry block with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {entry file} templates/maintain-contents.md`. The entry file holds the heading `## {owner}/{repo}`, a blank line, then one bullet per field in `templates/maintain-contents.md`'s order — 存取庫、維護方式、維護起始日、維護截止日、前次維護時間 — each written `- {欄位名}:{值}` with a full-width colon. **The third argument is the H2 heading, and for `MAINTAIN` that heading is the repository's own `{owner}/{repo}`**, matching the entry file's own heading byte for byte. `MAINTAIN` has no content page, so its heading cannot be a page name: a `MAINTAIN_{HASH}` heading would name a page that does not exist, while `{owner}/{repo}` never drifts and keys just as reliably. The `1` is the column number of the old table column that carries this entry's identity — the 存取庫 column, the only column `MAINTAIN` has that identifies a row, and it holds no link, so the conversion takes its plain text. It is used only for the automatic conversion: while the page on the wiki is still a markdown table the script reads the heading out of that column, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this repository's existing entry gets appended as a new one, one repository ends up with two blocks, and the old block is never updated again. Required: repository `{owner}/{repo}`, maintenance method, start date. Optional: end date (NULL = maintain forever), last-maintained time. `MAINTAIN` has no content page — this directory page is the whole record — so branch on the exit code per "Contents pages are appended, never overwritten" below and never hand-edit it.
6. Completion condition: the chosen delivery format has actually been produced and you have reported where it landed (wiki page name, or the comment URL); on the wiki-page format the `wiki-url` call returned 0 and its URL is the one in the 交付頁 bullet, with any non-zero code branched on as sub-step 12.3 says; every link written by this step was cleared by a `link-check.sh` run that exited 0; **and** the user has answered the maintenance question with a chosen registration saved on the wiki.
13. **Stage report — the last thing this stage does, including every early stop** (the work package gate blocked, no work package was selectable, the source branch was missing from the remote, a wiki read or write failed). Run `tools/stage-report.sh implement` with: 13. **Stage report — the last thing this stage does, including every early stop** (the work package gate blocked, no work package was selectable, the source branch was missing from the remote, a wiki read or write failed). Run `tools/stage-report.sh implement` with:
- one `--page TYPE:{page}` per wiki page this run wrote — `ANALYZE_{HASH}`, `DELIVER_{HASH}` and `DELIVER_CONTENTS`, `MAINTAIN_CONTENTS`; - one `--page TYPE:{page}` per wiki page this run wrote — `--page ANALYZE:ANALYZE_{HASH}`, `--page DELIVER:DELIVER_{HASH}`, `--page CONTENTS:DELIVER_CONTENTS`, `--page CONTENTS:MAINTAIN_CONTENTS`. **Every directory page takes the `CONTENTS` type**: the script resolves each page's repo from the TYPE you pass, and a directory page passed under its old type resolves the wrong repo and prints no URL;
- `--worklog` and `--worklog-heading` pointing at the entry step 11.4 wrote — following the Rules section's one-task-one-entry rule, a stage that finished anything already has one. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any task finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log; - `--worklog` and `--worklog-heading` pointing at the entry step 11.4 wrote — following the Rules section's one-task-one-entry rule, a stage that finished anything already has one. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any task finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log;
- `--worktree {path} --source-branch {name} --work-branch {name} --pr {url}` — the script reads the commit count, the push state and whether the source branch exists on the remote by itself, so pass the names, not your own count. - `--worktree {path} --source-branch {name} --work-branch {name} --pr {url}` — the script reads the commit count, the push state and whether the source branch exists on the remote by itself, so pass the names, not your own count.
Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and it names the worktree, all three branches and every wiki page this run wrote. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and it names the worktree, all three branches and every wiki page this run wrote.
14. **Write this run's `skill-end` status event — the very last thing this stage does, right after step 13, on every path including every early stop.** Run `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:implement {status} {exit} [detail]`, naming the script the way this stage already names `jsc-hooks/hooks/sdlc-gate.sh` in step 1. The matching `skill-start` event is written by jsc-hooks on its own, so this step owes only the `end`: a hook fires on the skill tool call and this stage's work happens in the model turns after it, so **no hook can see how this run ended**. A `start` with no `end` is what an aborted run looks like in the record, and this stage is the one that most often runs for hours before it stops, so the missing `end` is exactly the case worth telling apart.
## Contents pages are appended, never overwritten `{status}` is one of five words, never a sixth:
`DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` are shared directories: every row on them belongs to somebody's work package or repository, and this run reads none of those rows from anywhere else. So every write to them is an upsert of one row on top of the content just read — add the row if missing, otherwise refresh it, then `wiki-put` the whole page. Whole-page overwrite is forbidden, and a row this run does not own stays untouched. | Status | When `implement` reports it |
| --- | --- |
| `ok` | Every step's completion condition is met: the gate passed, the claimed package's todos all show `[x]`, both closing audits cleared (a reported API-document skip counts as cleared), `pr-watch.sh` returned 0 with `wp-gate.sh check` reporting `status=merged`, the work log entries are saved, the delivery was produced, the maintenance question was answered, and `tools/stage-report.sh` exited 0 |
| `blocked` | A gate stopped this run before any package was worked on: `sdlc-gate.sh lock implement` exited non-zero because the model carries no `coding` tag, every candidate came back `status=blocked` from `wp-gate.sh check-deps` because a dependency's PR is not merged, `wp-gate.sh owns` answered `status=foreign` on the only PR left to settle, or the analysis page's source branch is missing from the remote (step 5.3). No code was written. Comment rounds that step 2 did finish are named in `[detail]`, because they are real work sitting behind a blocked verdict — but they do not turn it into `ok` |
| `failed` | Work started and then something did not complete: a `wp-gate.sh` call returned 3 (`status=missing-dep`, a gate that could not decide), `pr-watch.sh` returned 2 or 3, `wp-gate.sh check` reported `status=closed-unmerged`, an audit could not be brought to a passing verdict, or a wiki write did not land (`wiki-url` 5, 7 or 8; `link-check.sh` 1; `wiki-contents.sh` 1, 7 or 8) |
| `degraded` | The package itself finished — todos `[x]`, PR merged — but a closing item did not: `DELIVER_{HASH}` is saved while `DELIVER_CONTENTS` was not upserted, the maintenance registration went unrecorded on `wiki-contents.sh` exit 3, or `tools/stage-report.sh` exited 1 (no work log, or a link in its list does not answer) |
| `aborted` | The user stopped the run, or the run stopped itself because its premise did not hold — step 3 found no selectable work package, so there was nothing to implement |
That rests entirely on reading the old page back, so branch the `wiki-get` on its exit code: `{exit}` is the exit code of the script whose verdict decided the status — the gate's code for `blocked`, the failing script's code for `failed` and `degraded` — and `0` when nothing exited non-zero, `ok` and `aborted` included. `[detail]` is optional and Traditional Chinese per the STE100 rule: one line, no line break, naming what decided the status (for example 「相依工作包的 PR 未合併」 or 「交付目錄區塊未寫入」). The script truncates it at 200 characters, so put the short reason there and nothing else.
**A failure in this step never changes this stage's verdict.** The script is not found (jsc-hooks is not installed on this machine, or this CLI's layout puts it somewhere else) → skip the event quietly and carry on; nothing is reported to the user and no step is re-run. The three recording sub-commands are built to exit 0 even when the write fails, so a non-zero code here means only that the call itself was malformed (exit 2, a usage error) — fix the arguments once and, either way, never turn a merged work package into a failed stage because the record of it failed. Completion condition: one `skill-end` event has been written for this run, or the script could not be found and that skip is the reason no event exists.
## Every link is checked before it reaches a page
**One syntax.** Every link this stage writes — on `DELIVER_{HASH}`, in the analysis page's PR column, in the `DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` entry blocks, in an issue comment, in the stage report — is written as `[{text}]({url})`. The `{url}` is the absolute URL `gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
**Checked before it is written.** Collect every link the page, the entry block or the comment is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this step does | | Exit | What this step does |
| --- | --- | | --- | --- |
| 0 | the page is there — upsert this run's row into the content that came back, then write the whole page | | 0 | every link answers — write the page, upsert the entry block, post the comment |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template | | 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing | | 2 | usage error: not one URL was passed — pass the links and run it again |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged | | 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and a page rewritten on that reading loses the links that were fine |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" makes the step write a fresh template over a live directory, and every other work package's row is gone — the write carries no merge and no backup. `DELIVER_{HASH}` is the opposite case: it is a content page belonging to one work package, so writing it whole is correct. The distinction is the page, not the write. Completion condition: every page, entry block and comment this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code was branched on as this table says.
Completion condition: every contents-page write this stage made names the `wiki-get` exit code it branched on, and no page was created on any code other than 4. ## Contents pages are appended, never overwritten
`DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` are shared directories in the CONTENTS wiki repo: every H2 block on them belongs to somebody's work package or repository, and this run reads none of those blocks from anywhere else. So every write to them is an upsert of one block — never a whole-page overwrite, and never a block this run does not own. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, converts a page still holding a markdown table into blocks first, replaces the block whose H2 heading matches the key and appends at the end when none matches, then writes the page back.
Branch on its exit code:
| Exit | What this step does |
| --- | --- |
| 0 | the block is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the page content could not be assembled, or the write failed — report it as a failed write and go to step 13 as a failure. **A page holding no matching block is not this code**: with nothing to replace the script appends the block and exits 0 |
| 2 | an argument was rejected (unknown type, bad key-column number, missing entry file) — fix the argument and run it again; nothing was written |
| 3 | no CONTENTS wiki repo is configured — stop and report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set. A `DELIVER_{HASH}` page already written is saved and stays saved; the maintenance registration is not recorded anywhere else, so report it as unregistered |
| 4 | the directory page is absent and no template was passed. **Every call in this stage already passes that page's template, so this code does not come out of this skill's call** — a wrong template path is rejected as 2, not as 4. Seeing it anyway means the template file is not where the plugin puts it: confirm the plugin installation is complete and run it again. Never answer it by dropping the template argument |
| 7 | the key is invalid or lacks permission — stop and report the key problem; the script wrote nothing, which is what keeps every other block alive |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" would write a fresh template over a live directory, and every other work package's block is gone — the write carries no merge and no backup. `DELIVER_{HASH}` is the opposite case: it is a content page belonging to one work package and living in the DELIVER repo, so writing it whole is correct. The distinction is the page, not the write.
Completion condition: every directory-page write this stage made names the `wiki-contents.sh` exit code it branched on, and no directory page was created on any code other than 4.
## Rules ## Rules
+60 -13
View File
@@ -1,17 +1,30 @@
--- ---
name: maintain name: maintain
description: SDLC maintenance stage. Gate on capability tags enforced in code by sdlc-gate (maintenance requires no specific tag, but the actual model id must be determinable from the transcript), read projects still inside their maintenance window from MAINTAIN_CONTENTS, then run one sub agent per project: switch to develop or master, propose at least five maintenance actions, commit to a new branch, push, and PR. Write a jsc-log:worklog entry per finished project and update the last-maintained timestamp afterward, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written. Use for periodic upkeep of delivered projects inside their maintenance window; not for projects still mid-implementation or not yet registered in MAINTAIN_CONTENTS. description: 'SDLC maintenance stage. Gate on capability tags enforced in code by sdlc-gate (maintenance requires no specific tag, but the actual model id must be determinable from the transcript), read projects still inside their maintenance window from MAINTAIN_CONTENTS, one H2 block per project in the separate CONTENTS wiki repo, then run one sub agent per project: switch to develop or master, propose at least five maintenance actions, commit to a new branch, push, and PR. Write a jsc-log:worklog entry per finished project and update the last-maintained timestamp afterward through jsc-gitea/tools/wiki-contents.sh, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written. Use for periodic upkeep of delivered projects inside their maintenance window; not for projects still mid-implementation or not yet registered in MAINTAIN_CONTENTS.'
--- ---
# maintain # maintain
Goal: run routine maintenance for every project in the maintenance contents page. Goal: run routine maintenance for every project in the maintenance contents page.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Step 5 still runs after such a stop.
**`MAINTAIN_CONTENTS` lives in the CONTENTS wiki repo**, the one `jsc-gitea/tools/gitea.sh wiki-repo CONTENTS` resolves: `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to `JSC_WIKI_REPO_MAINTAIN`. `MAINTAIN` is the one page type with no content page, so this directory page is the whole record — read it and write it there, and nowhere else. **It is a bullet-list directory page, not a table**: one H2 block per project, the heading being that project's own `{owner}/{repo}` — `MAINTAIN` has no content page, so its heading cannot be a page name, and a `MAINTAIN_{HASH}` heading would name a page that does not exist, while `{owner}/{repo}` never drifts and keys just as reliably — and the fields — 存取庫、維護方式、維護起始日、維護截止日、前次維護時間 — a one-level bullet list under it, each written `- {欄位名}:{值}` with a full-width colon. Every link it carries is written as `[{text}]({url})`, and the `{url}` is the absolute URL from `gitea.sh wiki-url {that type's repo} {page}` — one link syntax, whichever wiki the page sits in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below. Branch on `wiki-url`'s exit code every time — this is the same branching `jsc-log:worklog` runs over the same call:
| Exit | What this stage does |
| --- | --- |
| 0 | use the URL it printed, verbatim |
| 4 | that page is not on the wiki — leave the bullet with the literal 「無」 and say so, or, when the page was supposed to have been written by this run, go back and write it before coming here again |
| 5 | the page exists but carries no `html_url` — stop and report it, and never assemble the URL by hand; a hand-built path is not the one Gitea serves |
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4 and never fill 「無」: the page is alive, and 「無」 records a live page as one that does not exist |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL: a bullet that is empty names a page nobody can open, and the next run rewrites that block as if it were correct.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Steps 5 and 6 still run after such a stop.
## Steps ## Steps
1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock maintain`. This stage requires no specific capability tag; the gate passes as long as the script can determine the actual model id. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict. 1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock maintain`. This stage requires no specific capability tag; the gate passes as long as the script can determine the actual model id. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict.
2. Read `MAINTAIN_CONTENTS` via `jsc-gitea:wiki` and filter projects **still inside their maintenance window**: start date ≤ today, and (end date is NULL or ≥ today). Completion condition: you have listed every in-window project with its `{owner}/{repo}` and window dates, or reported that none is in window and stopped. 2. Read `MAINTAIN_CONTENTS` via `jsc-gitea:wiki`, out of the CONTENTS wiki repo (`gitea.sh wiki-repo CONTENTS`, never the MAINTAIN one), and filter projects **still inside their maintenance window**: start date ≤ today, and (end date is NULL or ≥ today). **Read the page as H2 blocks, not table rows**: each `## {owner}/{repo}` heading is one project, and the window dates come from that block's `- 維護起始日:` and `- 維護截止日:` bullets while the repository comes from its `- 存取庫:` bullet. Build every option and every later `upsert` key from the heading of the block the project came out of, never from a hand-computed one. Completion condition: you have listed every in-window project with its `{owner}/{repo}`, its H2 heading and its window dates, or reported that none is in window and stopped.
3. **Align every project in one batch first, then run one sub agent per project.** Completion condition: every project listed in step 2 has its sub agent finished, and each one ends in either a PR link or a recorded skip reason. 3. **Align every project in one batch first, then run one sub agent per project.** Completion condition: every project listed in step 2 has its sub agent finished, and each one ends in either a PR link or a recorded skip reason.
1. **Batch prefetch, run by the main agent before any sub agent starts.** For every in-window project from step 2, run `git fetch --prune origin`, then put it on its maintenance branch and align it with `origin/{branch}`. **The projects are independent — run this batch concurrently**, and hand each sub agent the branch name and the aligned commit sha instead of letting it fetch again. Which branch that is, the remote-is-the-basis rule, the diverged case and the never-pull-never-reset rule all live in `references/branch.md`; never guess the branch name. From sub-step 3.2 onward the flow is one project at a time, sequential, so that 3.5's work log rule holds. Completion condition: every project's HEAD points at the same commit as `origin/{branch}`, or its gap is reported and that project is skipped and left out of the sub agent runs. 1. **Batch prefetch, run by the main agent before any sub agent starts.** For every in-window project from step 2, run `git fetch --prune origin`, then put it on its maintenance branch and align it with `origin/{branch}`. **The projects are independent — run this batch concurrently**, and hand each sub agent the branch name and the aligned commit sha instead of letting it fetch again. Which branch that is, the remote-is-the-basis rule, the diverged case and the never-pull-never-reset rule all live in `references/branch.md`; never guess the branch name. From sub-step 3.2 onward the flow is one project at a time, sequential, so that 3.5's work log rule holds. Completion condition: every project's HEAD points at the same commit as `origin/{branch}`, or its gap is reported and that project is skipped and left out of the sub agent runs.
2. **From here on, one project at a time, and each project's maintenance MUST run as a sub agent.** Propose **at least five** maintenance methods, then let the user pick per `jsc-ask:ask` rules — every option states its impact scope (which files it touches, whether it can break the build, how much review it costs). Candidates: 2. **From here on, one project at a time, and each project's maintenance MUST run as a sub agent.** Propose **at least five** maintenance methods, then let the user pick per `jsc-ask:ask` rules — every option states its impact scope (which files it touches, whether it can break the build, how much review it costs). Candidates:
@@ -26,23 +39,57 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
3. **A code comment states why the code is written this way; it never states where the work is tracked.** Issue numbers, commit hashes, branch names, people's names and `@` mentions stay out of every code comment this project's maintenance touches — including the comments the cleanup method rewrites. Full list and the allowed exceptions: `jsc-review/references/comment-scope.md`. Two passes already cover the diff, so **run no separate manual sweep of your own**: `jsc-hooks/hooks/comment-scope.sh` compares each file after it is written and prints a warning — fix the flagged line at once, then carry on — and `jsc-git:commit` sweeps the whole working tree again in step 3.4, before anything is committed. **Coverage is not the same on every CLI**: only claude gets the per-file warning as the file is written. On codex, kiro, copilot and antigravity the hook fires late — at the end of the turn on codex, at the next prompt submit on kiro, at the end of the session on copilot and antigravity — so the pre-commit sweep in step 3.4 is the only pass on all four that lands in time to keep a flagged comment out of the commit. Completion condition: every warning the hook printed is fixed, and the step 3.4 sweep reported no remaining comment line carrying an issue number, a commit hash, a branch name, a person's name or an `@` mention. 3. **A code comment states why the code is written this way; it never states where the work is tracked.** Issue numbers, commit hashes, branch names, people's names and `@` mentions stay out of every code comment this project's maintenance touches — including the comments the cleanup method rewrites. Full list and the allowed exceptions: `jsc-review/references/comment-scope.md`. Two passes already cover the diff, so **run no separate manual sweep of your own**: `jsc-hooks/hooks/comment-scope.sh` compares each file after it is written and prints a warning — fix the flagged line at once, then carry on — and `jsc-git:commit` sweeps the whole working tree again in step 3.4, before anything is committed. **Coverage is not the same on every CLI**: only claude gets the per-file warning as the file is written. On codex, kiro, copilot and antigravity the hook fires late — at the end of the turn on codex, at the next prompt submit on kiro, at the end of the session on copilot and antigravity — so the pre-commit sweep in step 3.4 is the only pass on all four that lands in time to keep a flagged comment out of the commit. Completion condition: every warning the hook printed is fixed, and the step 3.4 sweep reported no remaining comment line carrying an issue number, a commit hash, a branch name, a person's name or an `@` mention.
4. Commit the changes to a new branch per `jsc-git:commit`, push, then open a PR per `jsc-git:pr` back to the branch of step 3.1, passing it explicitly as the base. Completion condition: the PR exists, and you have reported it with the table format in `jsc-meta/references/pr-report.md`. 4. Commit the changes to a new branch per `jsc-git:commit`, push, then open a PR per `jsc-git:pr` back to the branch of step 3.1, passing it explicitly as the base. Completion condition: the PR exists, and you have reported it with the table format in `jsc-meta/references/pr-report.md`.
5. **One project's maintenance is one finished task — call `jsc-log:worklog` right after its PR is open.** A task is one of three things: one work package, one round of PR-comment fixes, or one standalone fix commit; this stage produces the third kind, one per project. Never let the stage end and then write a single catch-up entry, and never let a second project start before the first one's entry is saved — by then the elapsed time, the token counts and the difficulties are gone. Every entry appends to the same `LOG_{HASH}` page. Content parked earlier by `tools/stage-report.sh --pending-file` is merged into that same write and cleared only once the write succeeds; parked content is not a written log. Completion condition: this project's entry is saved on `LOG_{HASH}` before the next project's sub agent starts. 5. **One project's maintenance is one finished task — call `jsc-log:worklog` right after its PR is open.** A task is one of three things: one work package, one round of PR-comment fixes, or one standalone fix commit; this stage produces the third kind, one per project. Never let the stage end and then write a single catch-up entry, and never let a second project start before the first one's entry is saved — by then the elapsed time, the token counts and the difficulties are gone. Every entry appends to the same `LOG_{HASH}` page. Content parked earlier by `tools/stage-report.sh --pending-file` is merged into that same write and cleared only once the write succeeds; parked content is not a written log. Completion condition: this project's entry is saved on `LOG_{HASH}` before the next project's sub agent starts.
6. Update the project's last-maintained field (the zh-TW column 「前次維護時間」) in `MAINTAIN_CONTENTS` to today, per "Contents pages are appended, never overwritten" below: read the page back, change only this project's row (add it if it is missing), and write the whole page. Completion condition: `MAINTAIN_CONTENTS` shows today's date in 「前次維護時間」 for that project, every other project's row is byte-for-byte unchanged, and the `wiki-get` exit code the write branched on is named. 6. Update the project's last-maintained field (the zh-TW bullet 「前次維護時間」) in `MAINTAIN_CONTENTS` to today with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {entry file} templates/maintain-contents.md`, never by hand-editing the page. Rebuild that project's H2 block from the one the page already holds: keep the same `## {owner}/{repo}` heading, change only the `- 前次維護時間:` bullet, and keep every other bullet byte-for-byte as it was. **The third argument is that H2 heading — for `MAINTAIN` it is the project's own `{owner}/{repo}`**, taken from the block step 2 read, matching the entry file's own heading byte for byte; never substitute a `MAINTAIN_{HASH}` key, which would point at a page that does not exist. The `1` is the column number of the old table column that carries this entry's identity — the 存取庫 column, the only column `MAINTAIN` has that identifies a row, and it holds no link, so the conversion takes its plain text. It is used only for the automatic conversion: while the page on the wiki is still a markdown table the script reads the heading out of that column, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this project's existing entry gets appended as a new one, one project ends up with two blocks, and the old block is never updated again. **A block that carries a link goes through `jsc-gitea/tools/link-check.sh` before the upsert, and is upserted only on exit 0** — see "Every link is checked before it reaches a page" below. Branch on the upsert's exit code per "Contents pages are appended, never overwritten" below. Completion condition: the script exited 0, every link in the rebuilt block was cleared by a `link-check.sh` run that exited 0, `MAINTAIN_CONTENTS` shows today's date in 「前次維護時間」 for that project, and every other project's block is byte-for-byte unchanged.
4. The main agent reports the summary: maintenance methods applied per project, PR table rows, and failure reasons. The report and all generated wiki content, commits, and PR descriptions stay Traditional Chinese per the STE100 rule. Completion condition: the summary names every project read in step 2, each with its applied methods and either a PR table row or the reason it was skipped. 4. The main agent reports the summary: maintenance methods applied per project, PR table rows, and failure reasons. The report and all generated wiki content, commits, and PR descriptions stay Traditional Chinese per the STE100 rule. Completion condition: the summary names every project read in step 2, each with its applied methods and either a PR table row or the reason it was skipped.
5. **Stage report — the last thing this stage does, including when no project was in window, and when a wiki read or write failed.** Run `tools/stage-report.sh maintain` with one `--page MAINTAIN:{page}` per wiki page this run wrote (`MAINTAIN_CONTENTS` counts), plus `--worklog` and `--worklog-heading` pointing at the entries step 3.5 wrote. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any project finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it. 5. **Stage report — the last thing this stage does, including when no project was in window, and when a wiki read or write failed.** Run `tools/stage-report.sh maintain` with one `--page TYPE:{page}` per wiki page this run wrote — that is `--page CONTENTS:MAINTAIN_CONTENTS`, under the `CONTENTS` type, because the script resolves each page's repo from the TYPE you pass and `MAINTAIN:` would resolve the wrong repo and print no URL — plus `--worklog` and `--worklog-heading` pointing at the entries step 3.5 wrote. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any project finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it.
6. **Write this run's `skill-end` status event — the very last thing this stage does, right after step 5, on every path including when no project was in window.** Run `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:maintain {status} {exit} [detail]`, naming the script the way this stage already names `jsc-hooks/hooks/sdlc-gate.sh` in step 1. The matching `skill-start` event is written by jsc-hooks on its own, so this step owes only the `end`: a hook fires on the skill tool call and this stage's work happens in the model turns after it, so **no hook can see how this run ended**. A `start` with no `end` is what an aborted run looks like in the record, and a stage that often ends with nothing to do needs that difference recorded, not guessed.
`{status}` is one of five words, never a sixth:
| Status | When `maintain` reports it |
| --- | --- |
| `ok` | Every step's completion condition is met: the gate passed, every in-window project ran its sub agent and ended in a PR, each finished project has its work log entry, `MAINTAIN_CONTENTS` shows today in 「前次維護時間」 for every one of them, and `tools/stage-report.sh` exited 0 |
| `blocked` | A check that lives in code stopped the run before any maintenance: `sdlc-gate.sh lock maintain` exited non-zero because the script could not determine the actual model id from the transcript, which is the one thing this stage's gate asks for; or step 3.1 found every in-window project out of step with `origin/{branch}`, so all of them were skipped and not one maintenance action ran. Nothing was maintained, so this is **never `failed`** — both are the guard working |
| `failed` | Maintenance ran and then a write did not land: `wiki-contents.sh` returned 1, 7 or 8 over `MAINTAIN_CONTENTS`, `wiki-url` returned 5, 7 or 8, or `link-check.sh` returned 1 so the block was never written. `MAINTAIN` has no content page, so a block that never lands loses the whole wiki record of this run — that is why it is `failed` and not `degraded` |
| `degraded` | Some projects came through and some did not: one project was skipped for a branch gap or a method that could not be applied while the others got their PR, or every project got its PR while `wiki-contents.sh` returned 3 so no 「前次維護時間」 was updated, or `tools/stage-report.sh` exited 1 (no work log, or a link in its list does not answer) |
| `aborted` | The user stopped the run, or the run stopped itself because its premise did not hold — step 2 found no project inside its maintenance window, so there was nothing to maintain |
`{exit}` is the exit code of the script whose verdict decided the status — the gate's code for `blocked`, the failing script's code for `failed` and `degraded` — and `0` when nothing exited non-zero, `ok` and `aborted` included. `[detail]` is optional and Traditional Chinese per the STE100 rule: one line, no line break, naming what decided the status (for example 「無專案在維護期內」 or 「兩個專案與遠端有落差已略過」). The script truncates it at 200 characters, so put the short reason there and nothing else.
**A failure in this step never changes this stage's verdict.** The script is not found (jsc-hooks is not installed on this machine, or this CLI's layout puts it somewhere else) → skip the event quietly and carry on; nothing is reported to the user and no step is re-run. The three recording sub-commands are built to exit 0 even when the write fails, so a non-zero code here means only that the call itself was malformed (exit 2, a usage error) — fix the arguments once and, either way, never turn a stage that opened its PRs into a failed one because the record of it failed. Completion condition: one `skill-end` event has been written for this run, or the script could not be found and that skip is the reason no event exists.
## Every link is checked before it reaches a page
**One syntax.** Every link this stage writes — in the `MAINTAIN_CONTENTS` entry block, in a PR description, in the stage report — is written as `[{text}]({url})`. The `{url}` is the absolute URL `gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
**Checked before it is written.** Collect every link the entry block is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this stage does |
| --- | --- |
| 0 | every link answers — upsert the entry block |
| 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 2 | usage error: not one URL was passed — pass the links and run it again |
| 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and `MAINTAIN_CONTENTS` is the whole record of this stage, so a block rewritten on that reading loses links nothing else holds |
Completion condition: every entry block this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code was branched on as this table says.
## Contents pages are appended, never overwritten ## Contents pages are appended, never overwritten
`MAINTAIN_CONTENTS` is a shared directory: every row on it belongs to somebody's project, and this run reads none of those rows from anywhere else. So step 3.6 is an upsert of one row on top of the content just read — add the row if missing, otherwise refresh its 前次維護時間 — then `wiki-put` the whole page. Whole-page overwrite is forbidden, and a project this run did not maintain keeps its row untouched. `MAINTAIN_CONTENTS` is a shared directory in the CONTENTS wiki repo: every H2 block on it belongs to somebody's project, and this run reads none of those blocks from anywhere else. So step 3.6 is an upsert of one block — never a whole-page overwrite, and a project this run did not maintain keeps its block untouched. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, converts a page still holding a markdown table into blocks first, replaces the block whose H2 heading matches the key and appends at the end when none matches, then writes the page back.
That rests entirely on reading the old page back, so branch the `wiki-get` on its exit code: Branch on its exit code:
| Exit | What this step does | | Exit | What this step does |
| --- | --- | | --- | --- |
| 0 | the page is there — upsert this project's row into the content that came back, then write the whole page | | 0 | the block is in place — carry the `updated` or `added` word it printed into the stage report |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template, and it also means step 2 had no project to maintain | | 1 | the page content could not be assembled, or the write failed — report it as a failed write and go to step 5 as a failure. **A page holding no matching block is not this code**: with nothing to replace the script appends the block and exits 0 |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing | | 2 | an argument was rejected (unknown type, bad key-column number, missing entry file) — fix the argument and run it again; nothing was written |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged | | 3 | no CONTENTS wiki repo is configured — stop and report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set. `MAINTAIN` has no content page, so nothing of this stage's record survives elsewhere: the PR is open but the maintenance date is unrecorded, and it is reported that way |
| 4 | the directory page is absent and no template was passed. **Step 3.6 always passes `templates/maintain-contents.md`, so this code does not come out of this skill's call** — a wrong template path is rejected as 2, not as 4. Seeing it anyway means the template file is not where the plugin puts it: confirm the plugin installation is complete and run it again, and never answer it by dropping the template argument. An absent page also means step 2 had no project to maintain |
| 7 | the key is invalid or lacks permission — stop and report the key problem; the script wrote nothing, which is what keeps every other project's block alive |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" makes step 3.6 write a fresh template over a live directory, and every other project's maintenance window is gone — the write carries no merge and no backup. Content pages, which belong to one subject each, are the opposite case and may be rewritten whole. The distinction is the page, not the write. Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" would write a fresh template over a live directory, and every other project's maintenance window is gone — the write carries no merge and no backup. Content pages, which belong to one subject each and live in their own type's repo, are the opposite case and may be rewritten whole. The distinction is the page, not the write.
Completion condition: the `MAINTAIN_CONTENTS` write names the `wiki-get` exit code it branched on, and no page was created on any code other than 4. Completion condition: the `MAINTAIN_CONTENTS` write names the `wiki-contents.sh` exit code it branched on, and the directory page was created on no code other than 4.
+64 -16
View File
@@ -1,6 +1,6 @@
--- ---
name: plan name: plan
description: SDLC planning stage. Gate on capability tags enforced in code by sdlc-gate (plan requires reasoning-max, verified against the transcript's actual model id), pick or create a plan from PLAN_CONTENTS, then run a decision tree until goal, scope, and feasibility reach consensus. Produce user stories into wiki page PLAN_{HASH}, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written. Logic only - never write code or modify files. Use when the user wants to start or refine a plan; not for analysis or implementation. description: SDLC planning stage. Gate on capability tags enforced in code by sdlc-gate (plan requires reasoning-max, verified against the transcript's actual model id), pick or create a plan from PLAN_CONTENTS, which sits in the separate CONTENTS wiki repo, then run a decision tree until goal, scope, and feasibility reach consensus. Produce user stories into wiki page PLAN_{HASH} in the PLAN wiki repo, upsert this plan's H2 block on that directory page through jsc-gitea/tools/wiki-contents.sh with an absolute wiki-url link, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written. Logic only - never write code or modify files. Use when the user wants to start or refine a plan; not for analysis or implementation.
--- ---
# plan # plan
@@ -8,14 +8,17 @@ description: SDLC planning stage. Gate on capability tags enforced in code by sd
Goal: create or extend the wiki plan page `PLAN_{HASH}`. Goal: create or extend the wiki plan page `PLAN_{HASH}`.
This skill is a **logic-only** stage: never output code, and **never modify any file**. This skill is a **logic-only** stage: never output code, and **never modify any file**.
`{HASH}` = the shared wiki hash for `{owner}/{repo}` used to build the `PLAN_{HASH}` page name, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). `{HASH}` = the shared wiki hash for `{owner}/{repo}` used to build the `PLAN_{HASH}` page name, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). It prints the **full 40-character uppercase SHA-1** of its input — no truncation to 8 characters, no prefix rewrite. Never shorten it by hand: a shortened name points at a page nobody else writes to.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Step 8 still runs after such a stop.
**The two pages this stage touches live in two different wiki repos.** The content page `PLAN_{HASH}` sits in the repo `jsc-gitea/tools/gitea.sh wiki-repo PLAN` resolves. The directory page `PLAN_CONTENTS` sits in the repo `gitea.sh wiki-repo CONTENTS` resolves — `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to `JSC_WIKI_REPO_PLAN`. **`PLAN_CONTENTS` is a bullet-list directory page, not a table**: one H2 block per plan, the heading being that plan's content page **actual** name — the page this stage itself wrote, never a `PLAN_{HASH}` formula — and the fields a one-level bullet list under it, each written `- {欄位名}:{值}` in the order `templates/plan-contents.md` gives. The entry links the plan page by the absolute URL from `gitea.sh wiki-url {PLAN repo} PLAN_{HASH}`, written as `[{text}]({url})` — one link syntax, whichever wiki the two pages sit in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Steps 8 and 9 still run after such a stop.
## Steps ## Steps
1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock plan`. This stage requires the `reasoning-max` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict. 1. **Model gate and stage lock** — run `jsc-cli/tools/model-tags.sh sync`, then `jsc-hooks/hooks/sdlc-gate.sh lock plan`. This stage requires the `reasoning-max` capability tag. Rules: `references/model-gate.md`. Completion condition: the script exited 0, and you have reported the stage, the required tag, the actual model id it read from the transcript, and the verdict.
2. Read `PLAN_CONTENTS` via `jsc-gitea:wiki` and list the plans whose status is the literal 「未分析」 (not analyzed), with names and HASH. Completion condition: you have listed every 未分析 plan with its name and HASH, or reported that none exists. 2. Read `PLAN_CONTENTS` via `jsc-gitea:wiki`, out of the CONTENTS wiki repo (`gitea.sh wiki-repo CONTENTS`, never the PLAN one), and list the plans whose status is the literal 「未分析」 (not analyzed), with names and HASH. **Read it as H2 blocks, not table rows**: each `## PLAN_{HASH}` heading is one plan and is that plan's content page name, and its fields — 計畫名稱、計畫頁、存取庫、HASH、狀態、建立時間 — are the bullets under that heading. A plan is 未分析 when its `- 狀態:` bullet reads 「未分析」. Completion condition: you have listed every 未分析 plan with its name and HASH, or reported that none exists.
3. Let the user choose per `jsc-ask:ask` rules: **extend an existing plan** (list the not-analyzed plans as options) or **create a new plan**. State the impact scope on every option. Completion condition: the user has picked one option explicitly, and you have named the `PLAN_{HASH}` page this run writes to. 3. Let the user choose per `jsc-ask:ask` rules: **extend an existing plan** (list the not-analyzed plans as options) or **create a new plan**. **Build each option from one H2 block**: the H2 heading gives the page name and the HASH, and the option text comes from that block's `- 計畫名稱:` and `- 建立時間:` bullets. State the impact scope on every option. Completion condition: the user has picked one option explicitly, and you have named the `PLAN_{HASH}` page this run writes to.
4. **Keep questioning until consensus** — rules in `references/consensus.md`, which is the single authority for both planning and analysis. Cover all three items: 4. **Keep questioning until consensus** — rules in `references/consensus.md`, which is the single authority for both planning and analysis. Cover all three items:
- Goal: the problem to solve and the criteria for success. - Goal: the problem to solve and the criteria for success.
- Scope: what is included, what is excluded, which repositories are involved. - Scope: what is included, what is excluded, which repositories are involved.
@@ -25,31 +28,76 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
Completion condition: all three items are settled under both conditions of `references/consensus.md` — no remaining unknown that would change the output, **and** the user's explicit confirmation of the summary you read back. Completion condition: all three items are settled under both conditions of `references/consensus.md` — no remaining unknown that would change the output, **and** the user's explicit confirmation of the summary you read back.
5. Turn the consensus into **user stories** (the zh-TW pattern 「身為⋯⋯我想要⋯⋯以便⋯⋯」), one per line. Completion condition: every consensus item is covered by at least one user story in that pattern, and no user story rests on an unanswered question. 5. Turn the consensus into **user stories** (the zh-TW pattern 「身為⋯⋯我想要⋯⋯以便⋯⋯」), one per line. Completion condition: every consensus item is covered by at least one user story in that pattern, and no user story rests on an unanswered question.
6. Apply `templates/plan-page.md` to create or update the plan page, and write it back via `jsc-gitea:wiki`. The page content is Traditional Chinese, exactly as the template dictates. Completion condition: the page is saved on the wiki and carries every section the template dictates — goal, scope, feasibility, user stories and the consensus summary — with no placeholder left unfilled. 6. Apply `templates/plan-page.md` to create or update the plan page **in the PLAN wiki repo**, and write it back via `jsc-gitea:wiki`. The page content is Traditional Chinese, exactly as the template dictates. **Every link on that page is written as `[{文字}]({連結})` and passes `jsc-gitea/tools/link-check.sh` before the write** — see "Every link is checked before it reaches a page" below. Completion condition: the page is saved on the wiki and carries every section the template dictates — goal, scope, feasibility, user stories and the consensus summary — with no placeholder left unfilled, and the page holds no link that the check did not clear.
7. Upsert this plan's row in `PLAN_CONTENTS` using the entry format of `templates/plan-contents.md`, with status set to the literal 「未分析」 — add the row if missing, otherwise refresh it. Read the page back first and write the whole page, per "Contents pages are appended, never overwritten" below; never overwrite it wholesale, and never touch a row belonging to another plan. Completion condition: `PLAN_CONTENTS` shows this plan's row with the literal 「未分析」, every other row is byte-for-byte unchanged, and the `wiki-get` exit code the write branched on is named. 7. Upsert this plan's H2 entry block in `PLAN_CONTENTS` with `jsc-gitea/tools/wiki-contents.sh`; never hand-edit the directory page. **Take the plan page's absolute link from `gitea.sh wiki-url {PLAN repo} PLAN_{HASH}` first, and branch on that command's exit code before the entry is built** — the same branching `jsc-log:worklog` runs over this call:
8. **Stage report — the last thing this stage does, including every early stop** (the model gate blocked, no plan was selectable, a wiki read or write failed). Run `tools/stage-report.sh plan` with one `--page PLAN:{page}` per wiki page this run wrote (`PLAN_{HASH}` and `PLAN_CONTENTS` both count), plus `--worklog` and `--worklog-heading` when a work log entry exists. No work log yet: write this stage's log content to a file and pass `--pending-file {file} --log-hash {HASH}` so it is held for the next `jsc-log:worklog` run. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it.
| Exit | What this step does |
| --- | --- |
| 0 | use the URL it printed, verbatim |
| 4 | the page is not on the wiki, so the step 6 write has not landed — go back to step 6 and come here again only once the page is saved |
| 5 | the page exists but carries no `html_url` — stop and report it, and never assemble the URL by hand; a hand-built path is not the one Gitea serves |
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4: the plan page is alive, and treating it as absent records a live page as one that was never written |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL: an entry whose link bullet is empty is a directory entry that points nowhere, and the next run overwrites it as if it were correct. **Then check that URL with `jsc-gitea/tools/link-check.sh` and build the entry only on exit 0** — see "Every link is checked before it reaches a page" below; a link that does not answer never goes into a directory everyone else reads. Then build one file holding the single H2 block from `templates/plan-contents.md`: the heading `## {plan page name}`, a blank line, then one bullet per field in the template's order — 計畫名稱、計畫頁 (that absolute link written as `[{文字}]({連結})`)、存取庫、HASH、狀態 (the literal 「未分析」)、建立時間 — each written `- {欄位名}:{值}` with a full-width colon. Produce that file per Hard limits, with a Bash heredoc or `mktemp`, never with `Write` or `Edit`. Then run `jsc-gitea/tools/wiki-contents.sh upsert PLAN 2 {plan page name} {entry file} templates/plan-contents.md`. **The third argument is the H2 heading, that is the plan page's actual name — the very page this step just wrote** — it must match the entry file's own heading byte for byte, because a heading typed differently appends a second block for the same plan. Take the name you actually saved; never build the key from a `PLAN_{HASH}` formula, because the real page names carry whatever shape the write produced, not `{TYPE}_` plus 40 characters. The `2` is the column number of the old table column that holds the content-page link — the 計畫頁 column — and it is used only for the automatic conversion: while the page on the wiki is still a markdown table, the script takes the last path segment of that column's link URL as the H2 heading, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this plan's existing entry gets appended as a new one, one plan ends up with two blocks, and the old block is never updated again. Branch on the exit code per "Contents pages are appended, never overwritten" below. Completion condition: `wiki-url` returned 0 and its URL is the one in the entry, `link-check.sh` returned 0 over that URL, the upsert exited 0, `PLAN_CONTENTS` shows this plan's block under that page name with the literal 「未分析」 and that absolute plan-page link, and you have reported all three exit codes plus whether the script printed `updated` or `added`.
8. **Stage report — the last thing this stage does, including every early stop** (the model gate blocked, no plan was selectable, a wiki read or write failed). Run `tools/stage-report.sh plan` with one `--page TYPE:{page}` per wiki page this run wrote — `--page PLAN:PLAN_{HASH}` for the content page and `--page CONTENTS:PLAN_CONTENTS` for the directory page, because the script resolves each page's repo from the TYPE you pass and the two pages no longer share one — plus `--worklog` and `--worklog-heading` when a work log entry exists. No work log yet: write this stage's log content to a file — with a Bash heredoc or `mktemp` per Hard limits, never with `Write` or `Edit` — and pass `--pending-file {file} --log-hash {HASH}` so it is held for the next `jsc-log:worklog` run. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it.
9. **Write this run's `skill-end` status event — the very last thing this stage does, right after step 8, on every path including every early stop.** Run `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:plan {status} {exit} [detail]`, naming the script the way this stage already names `jsc-hooks/hooks/sdlc-gate.sh` in step 1. The matching `skill-start` event is written by jsc-hooks on its own, so this step owes only the `end`: a hook fires on the skill tool call and this stage's work happens in the model turns after it, so **no hook can see how this run ended**. A `start` with no `end` is what an aborted run looks like in the record, and this step is the only thing that keeps a finished run from looking like one.
`{status}` is one of five words, never a sixth:
| Status | When `plan` reports it |
| --- | --- |
| `ok` | Every step's completion condition is met: the gate passed, all three consensus items were confirmed by the user, `PLAN_{HASH}` is saved with no placeholder left, the `PLAN_CONTENTS` entry block carries the literal 「未分析」 and a checked absolute link, and `tools/stage-report.sh` exited 0 |
| `blocked` | Step 1's model gate stopped the run: `sdlc-gate.sh lock plan` exited non-zero because the model running this stage carries no `reasoning-max` tag. Nothing was planned, so this is **never `failed`** — the gate stopping an underpowered model is the gate working, and recording it as a failure sends the next reader hunting for a defect that is not there |
| `failed` | The run got past the gate and then a write did not land: the `PLAN_{HASH}` write failed, `wiki-url` returned 5, 7 or 8, `link-check.sh` returned 1 so the page was never written, or `wiki-contents.sh` returned 1, 7 or 8 while the plan page is also unsaved |
| `degraded` | The plan page is saved but the directory did not follow it: `wiki-contents.sh` returned 1, 3, 7 or 8 over `PLAN_CONTENTS`, or `tools/stage-report.sh` exited 1 (no work log, or a link in its list does not answer). The plan exists; what is missing is the directory entry that lets anyone find it |
| `aborted` | The user stopped the run, or the run stopped itself because its premise did not hold — no plan was selectable and the user wanted no new one, or the consensus rounds ended with no agreement, so no user story was written |
`{exit}` is the exit code of the script whose verdict decided the status — the gate's code for `blocked`, the failing script's code for `failed` and `degraded` — and `0` when nothing exited non-zero, `ok` and `aborted` included. `[detail]` is optional and Traditional Chinese per the STE100 rule: one line, no line break, naming what decided the status (for example 「模型能力標籤不符」 or 「目錄頁未更新」). The script truncates it at 200 characters, so put the short reason there and nothing else.
**A failure in this step never changes this stage's verdict.** The script is not found (jsc-hooks is not installed on this machine, or this CLI's layout puts it somewhere else) → skip the event quietly and carry on; nothing is reported to the user and no step is re-run. The three recording sub-commands are built to exit 0 even when the write fails, so a non-zero code here means only that the call itself was malformed (exit 2, a usage error) — fix the arguments once and, either way, never turn a finished stage into a failed one because the record of it failed. Completion condition: one `skill-end` event has been written for this run, or the script could not be found and that skip is the reason no event exists.
## Every link is checked before it reaches a page
**One syntax.** Every link this stage writes — on `PLAN_{HASH}`, in the `PLAN_CONTENTS` entry block, in the stage report — is written as `[{文字}]({連結})`. The `{連結}` is the absolute URL `jsc-gitea/tools/gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
**Checked before it is written.** Collect every link the page is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write the page only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this stage does |
| --- | --- |
| 0 | every link answers — write the page |
| 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 2 | usage error: not one URL was passed — pass the links and run it again |
| 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and a page rewritten on that reading loses the links that were fine |
Completion condition: every page this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code was branched on as this table says.
## Contents pages are appended, never overwritten ## Contents pages are appended, never overwritten
`PLAN_CONTENTS` is a shared directory: every row on it belongs to somebody's plan, and this run reads none of those rows from anywhere else. So the write is an upsert of one row on top of the content just read — add the row if missing, otherwise refresh it, then `wiki-put` the whole page. Whole-page overwrite is forbidden. `PLAN_CONTENTS` is a shared directory in the CONTENTS wiki repo: every H2 block on it belongs to somebody's plan, and this run reads none of those blocks from anywhere else. So the write is an upsert of one block on top of the content just read — never a whole-page overwrite, and never a block that belongs to another plan. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, converts a page still holding a markdown table into blocks first, replaces the block whose H2 heading matches the key and appends at the end when none matches, then writes the page back.
That rests entirely on reading the old page back, so branch the `wiki-get` on its exit code: Branch on its exit code:
| Exit | What this step does | | Exit | What this step does |
| --- | --- | | --- | --- |
| 0 | the page is there — upsert this plan's row into the content that came back, then write the whole page | | 0 | the block is in place — carry the `updated` or `added` word it printed into the stage report |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template | | 1 | the page content could not be assembled, or the write failed — report it as a failed write and go to step 8 as a failure. **A page holding no matching block is not this code**: with nothing to replace the script appends the block and exits 0 |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing | | 2 | an argument was rejected (unknown type, bad key-column number, missing entry file) — fix the argument and run it again; nothing was written |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged | | 3 | no CONTENTS wiki repo is configured — stop and report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set. The plan page itself is saved and stays saved |
| 4 | the directory page is absent and no template was passed. **Step 7 always passes `templates/plan-contents.md`, so this code does not come out of this skill's call** — a wrong template path is rejected as 2, not as 4. Seeing it anyway means the template file is not where the plugin puts it: confirm the plugin installation is complete and run it again. Never answer it by dropping the template argument |
| 7 | the key is invalid or lacks permission — stop and report the key problem; the script wrote nothing, which is what keeps the other plans' blocks alive |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" makes step 7 write a fresh template over a live directory, and every other plan's row is gone — the write carries no merge and no backup. `PLAN_{HASH}` is the opposite case: it is a content page belonging to this one plan, so step 6 rewriting it whole is correct. The distinction is the page, not the write. Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" would write a fresh template over a live directory, and every other plan's block is gone — the write carries no merge and no backup. `PLAN_{HASH}` is the opposite case: it is a content page belonging to this one plan and living in the PLAN repo, so step 6 rewriting it whole is correct. The distinction is the page, not the write.
Completion condition: the `PLAN_CONTENTS` write names the `wiki-get` exit code it branched on, and no page was created on any code other than 4. Completion condition: the `PLAN_CONTENTS` write names the `wiki-contents.sh` exit code it branched on, and no directory page was created on any code other than 4.
## Hard limits ## Hard limits
- Never output a code snippet. - Never output a code snippet.
- **Never modify any file in the working directory.** This limit is enforced in code where the CLI allows it: `jsc-hooks/hooks/write-guard.sh` in `stage` mode runs as a `PreToolUse` hook and blocks `Write`, `Edit` and `MultiEdit` while this stage's lock exists — the same lock state `jsc-hooks/hooks/sdlc-gate.sh lock plan` writes in step 1. **Only claude has `PreToolUse`.** Codex, copilot, antigravity and kiro never reach that hook, so on those four CLIs this line is the only thing holding the limit. - **Never modify any file in the working directory.** This limit is enforced in code where the CLI allows it: `jsc-hooks/hooks/write-guard.sh` in `stage` mode runs as a `PreToolUse` hook and blocks `Write`, `Edit` and `MultiEdit` while this stage's lock exists — the same lock state `jsc-hooks/hooks/sdlc-gate.sh lock plan` writes in step 1. **Only claude has `PreToolUse`.** Codex, copilot, antigravity and kiro never reach that hook, so on those four CLIs this line is the only thing holding the limit.
- **The entry file of step 7 and the pending-file of step 8 are produced with a Bash heredoc or `mktemp`, in a temporary directory — never with the `Write` or `Edit` tool.** That gate blocks on the stage lock alone and never looks at the path, so a `Write` of either file is blocked by this stage's own lock and the stage cannot finish: the directory entry never gets written and this stage's log content is never parked. Both files are scratch input to a script, they live outside the working directory, and writing them this way keeps the limit above intact — nothing in the working directory is touched.
- Never skip the decision tree and assume requirements. - Never skip the decision tree and assume requirements.
- Never stop questioning after one round; consensus is reached only under `references/consensus.md`, and the user says so. - Never stop questioning after one round; consensus is reached only under `references/consensus.md`, and the user says so.
+14 -4
View File
@@ -1,7 +1,17 @@
# 分析目錄 # 分析目錄
> 寫入語意:一列代表一份分析頁。寫入前先讀回整頁,該分析頁已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。 > 存放位置:本頁是目錄頁,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫(沒設定就退回 `JSC_WIKI_REPO`,都沒設定就停)。分析頁 `ANALYZE_{HASH}` 住在 `JSC_WIKI_REPO_ANALYZE` 那一邊,兩者不同存取庫。
> 版面:一個 H1 頁名、一段 `>` 引言,接著每一筆一個 H2 區塊。H2 標題就是該筆分析頁的實際頁名(示範區塊寫成 `ANALYZE_{HASH}` 只是佔位符,真正要填的是 `jsc-sdlc:analyze` 剛寫出去那一頁的頁名,線上實際長成 `ANALYZE_20260821_100552_104F0709` 這樣),標題不放連結、不加前後綴。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`。本頁不留 markdown 表格。
> 寫入語意:一個區塊代表一份分析頁。一律用 `jsc-gitea/tools/wiki-contents.sh upsert ANALYZE 2 {分析頁頁名} {區塊檔} {本範本}` 單筆寫回:該分析頁已經有區塊就更新那個區塊,沒有才附加一個區塊,整頁一起送出。禁止整頁覆蓋,也不得改動別人的區塊。參數語意:`2` 是舊表格裡持有內容頁連結那一欄的序號,也就是「分析頁」欄,只供自動轉檔用——本頁還是舊表格時,腳本從那一欄的連結網址取最後一段路徑當 H2 標題;本頁已經是條列版面就完全忽略它。這個序號填錯,轉出來的標題就跟鍵對不上,既有那一筆會被當成新的附加上去,同一份分析變成兩個區塊,舊區塊從此再也更新不到,所以不是死參數、也不能隨便填。`{分析頁頁名}` 是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `ANALYZE_{HASH}` 的公式;`{區塊檔}` 是整個 H2 區塊的 markdown(`## {分析頁頁名}` 那一行加各條欄位),不是列檔。
> 連結寫法:一律寫成 `[{文字}]({連結})`。分析頁欄位的連結放 `jsc-gitea/tools/gitea.sh wiki-url` 給的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就整個區塊不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
> HASH:由 `jsc-gitea/tools/hash-id` 對 `{owner}/{repo}` 算出的完整 40 碼大寫十六進位,不截短、不加前綴。
| 計畫名稱 | 分析頁 | HASH | 工作包 | 未完成項目 | 狀態 | ## ANALYZE_{HASH}
| --- | --- | --- | --- | --- | --- |
| {計畫名稱} | [[{計畫名稱}|ANALYZE_{HASH}]] | {HASH} | WP-01、WP-02 | {n} | 未完成 | - 計畫名稱:{計畫名稱}
- 分析頁:[{計畫名稱}](https://{gitea 主機}/{owner}/{repo}/wiki/ANALYZE_{HASH})
- HASH:{HASH}
- 工作包:WP-01、WP-02
- 未完成項目:{n}
- 狀態:未完成
+2 -1
View File
@@ -1,7 +1,8 @@
# 分析:{計畫名稱} # 分析:{計畫名稱}
> {PLAN 頁絕對網址}、{REPO 頁絕對網址} 由 `jsc-gitea/tools/gitea.sh wiki-url` 取得。 > {PLAN 頁絕對網址}、{REPO 頁絕對網址} 由 `jsc-gitea/tools/gitea.sh wiki-url` 取得。
> 跨頁型連結一律用絕對網址:不同頁型可能落在不同存取庫,`[[頁名]]` 只在同一個 wiki 內解析。 > 連結寫法:一律寫成 `[{文字}]({連結})`,連結一律用絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
- 頁名:`ANALYZE_{HASH}` - 頁名:`ANALYZE_{HASH}`
- HASH:`{HASH}` - HASH:`{HASH}`
+16 -4
View File
@@ -1,9 +1,21 @@
# 交付目錄 # 交付目錄
> 寫入語意:一列代表一個工作包的交付。寫入前先讀回整頁,該工作包已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。 > 存放位置:本頁是目錄頁,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫(沒設定就退回 `JSC_WIKI_REPO`,都沒設定就停)。交付頁 `DELIVER_{HASH}` 住在 `JSC_WIKI_REPO_DELIVER` 那一邊,兩者不同存取庫。
> 版面:一個 H1 頁名、一段 `>` 引言,接著每一筆一個 H2 區塊。H2 標題就是該筆交付頁的實際頁名(示範區塊寫成 `DELIVER_{HASH}` 只是佔位符,真正要填的是 `jsc-sdlc:implement` 剛寫出去那一頁的頁名),標題不放連結、不加前後綴。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`。本頁不留 markdown 表格。
> 寫入語意:一個區塊代表一個工作包的交付。一律用 `jsc-gitea/tools/wiki-contents.sh upsert DELIVER 5 {交付頁頁名} {區塊檔} {本範本}` 單筆寫回:該工作包已經有區塊就更新那個區塊,沒有才附加一個區塊,整頁一起送出。禁止整頁覆蓋,也不得改動別人的區塊。參數語意:`5` 是舊表格裡持有內容頁連結那一欄的序號,也就是「交付頁」欄,只供自動轉檔用——本頁還是舊表格時,腳本從那一欄的連結網址取最後一段路徑當 H2 標題;本頁已經是條列版面就完全忽略它。這個序號填錯,轉出來的標題就跟鍵對不上,既有那一筆會被當成新的附加上去,同一個工作包的交付變成兩個區塊,舊區塊從此再也更新不到,所以不是死參數、也不能隨便填。`{交付頁頁名}` 是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `DELIVER_{HASH}` 的公式;`{區塊檔}` 是整個 H2 區塊的 markdown(`## {交付頁頁名}` 那一行加各條欄位),不是列檔。
> 連結寫法:一律寫成 `[{文字}]({連結})`。交付頁欄位的連結放 `jsc-gitea/tools/gitea.sh wiki-url` 給的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就整個區塊不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
> HASH:由 `jsc-gitea/tools/hash-id` 對 `{owner}/{repo}` 加工作包編號(例 `plugins/sdlc#WP-01`)算出的完整 40 碼大寫十六進位,不截短、不加前綴。一個工作包一頁,彼此不覆蓋。
<!-- 交付欄:是 = 交付、交接工作包(WP-01),接手者要據此動工;否 = 實作類工作包。 --> <!-- 交付欄:是 = 交付、交接工作包(WP-01),接手者要據此動工;否 = 實作類工作包。 -->
| 計畫名稱 | 工作包 | 交付 | 交付型別 | 交付頁 | HASH | 存取庫 | 交付時間 | ## DELIVER_{HASH}
| --- | --- | --- | --- | --- | --- | --- | --- |
| {計畫名稱} | WP-01 {工作包名稱} | 是 | API 文件 | [[{WP-01 工作包名稱}|DELIVER_{HASH}]] | {HASH} | {owner}/{repo} | {yyyy-MM-dd HH:mm:ss} | - 計畫名稱:{計畫名稱}
- 工作包:WP-01 {工作包名稱}
- 交付:是
- 交付型別:API 文件
- 交付頁:[{WP-01 工作包名稱}](https://{gitea 主機}/{owner}/{repo}/wiki/DELIVER_{HASH})
- HASH:{HASH}
- 存取庫:{owner}/{repo}
- 交付時間:{yyyy-MM-dd HH:mm:ss}
+3 -1
View File
@@ -1,6 +1,8 @@
# 交付:{計畫名稱} {WP-nn} {工作包名稱} # 交付:{計畫名稱} {WP-nn} {工作包名稱}
> {ANALYZE 頁絕對網址} 由 `jsc-gitea/tools/gitea.sh wiki-url` 取得。跨頁型連結一律用絕對網址。 > {ANALYZE 頁絕對網址} 由 `jsc-gitea/tools/gitea.sh wiki-url` 取得。
> 連結寫法:一律寫成 `[{文字}]({連結})`,連結一律用絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
- 頁名:`DELIVER_{HASH}` - 頁名:`DELIVER_{HASH}`
- HASH:`{HASH}` - HASH:`{HASH}`
+12 -4
View File
@@ -1,9 +1,17 @@
# 維護目錄 # 維護目錄
> 寫入語意:一列代表一個受維護的存取庫。寫入前先讀回整頁,該存取庫已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。 > 存放位置:本頁是目錄頁,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫(沒設定就退回 `JSC_WIKI_REPO`,都沒設定就停)。`MAINTAIN` 只有目錄頁、沒有內容頁,所以整個型別的紀錄就在這一頁。
> 版面:一個 H1 頁名、一段 `>` 引言,接著每一筆一個 H2 區塊。`MAINTAIN` 沒有內容頁,所以 H2 標題就是該存取庫的 `{owner}/{repo}`,標題不放連結、不加前後綴。存取庫名不會漂移,當鍵一樣穩;反過來造一個 `MAINTAIN_{HASH}` 式的標題,等於指向一個不存在的頁。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`。本頁不留 markdown 表格。
> 寫入語意:一個區塊代表一個受維護的存取庫。一律用 `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {區塊檔} {本範本}` 單筆寫回:該存取庫已經有區塊就更新那個區塊,沒有才附加一個區塊,整頁一起送出。禁止整頁覆蓋,也不得改動別人的區塊。參數語意:`1` 是舊表格裡持有這一筆身分的那一欄的序號,也就是「存取庫」欄,只供自動轉檔用——本頁還是舊表格時,腳本靠這一欄取出 H2 標題(該欄沒有連結,就取格子純文字);本頁已經是條列版面就完全忽略它。這個序號填錯,轉出來的標題就跟鍵對不上,既有那一筆會被當成新的附加上去,同一個存取庫變成兩個區塊,舊區塊從此再也更新不到,所以不是死參數、也不能隨便填。`{owner}/{repo}` 是 H2 標題,用來找既有區塊,寫這個存取庫實際的 `{owner}/{repo}`;`{區塊檔}` 是整個 H2 區塊的 markdown(`## {owner}/{repo}` 那一行加各條欄位),不是列檔。
> 連結寫法:一律寫成 `[{文字}]({連結})`。要指向別的頁型(例如交付頁)時,連結放 `jsc-gitea/tools/gitea.sh wiki-url` 給的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就整個區塊不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
<!-- 維護截止日 NULL = 永久維護 --> <!-- 維護截止日 NULL = 永久維護 -->
| 存取庫 | 維護方式 | 維護起始日 | 維護截止日 | 前次維護時間 | ## {owner}/{repo}
| --- | --- | --- | --- | --- |
| {owner}/{repo} | {例:套件更新與安全掃描} | {yyyy-MM-dd} | NULL | {yyyy-MM-dd} | - 存取庫:{owner}/{repo}
- 維護方式:{例:套件更新與安全掃描}
- 維護起始日:{yyyy-MM-dd}
- 維護截止日:NULL
- 前次維護時間:{yyyy-MM-dd}
+14 -4
View File
@@ -1,7 +1,17 @@
# 計畫目錄 # 計畫目錄
> 寫入語意:一列代表一份計畫。寫入前先讀回整頁,該計畫已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。 > 存放位置:本頁是目錄頁,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫(沒設定就退回 `JSC_WIKI_REPO`,都沒設定就停)。計畫頁 `PLAN_{HASH}` 住在 `JSC_WIKI_REPO_PLAN` 那一邊,兩者不同存取庫。
> 版面:一個 H1 頁名、一段 `>` 引言,接著每一筆一個 H2 區塊。H2 標題就是該筆計畫頁的實際頁名(示範區塊寫成 `PLAN_{HASH}` 只是佔位符,真正要填的是 `jsc-sdlc:plan` 剛寫出去那一頁的頁名),標題不放連結、不加前後綴。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`。本頁不留 markdown 表格。
> 寫入語意:一個區塊代表一份計畫。一律用 `jsc-gitea/tools/wiki-contents.sh upsert PLAN 2 {計畫頁頁名} {區塊檔} {本範本}` 單筆寫回:該計畫已經有區塊就更新那個區塊,沒有才附加一個區塊,整頁一起送出。禁止整頁覆蓋,也不得改動別人的區塊。參數語意:`2` 是舊表格裡持有內容頁連結那一欄的序號,也就是「計畫頁」欄,只供自動轉檔用——本頁還是舊表格時,腳本從那一欄的連結網址取最後一段路徑當 H2 標題;本頁已經是條列版面就完全忽略它。這個序號填錯,轉出來的標題就跟鍵對不上,既有那一筆會被當成新的附加上去,同一份計畫變成兩個區塊,舊區塊從此再也更新不到,所以不是死參數、也不能隨便填。`{計畫頁頁名}` 是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `PLAN_{HASH}` 的公式;`{區塊檔}` 是整個 H2 區塊的 markdown(`## {計畫頁頁名}` 那一行加各條欄位),不是列檔。
> 連結寫法:一律寫成 `[{文字}]({連結})`。計畫頁欄位的連結放 `jsc-gitea/tools/gitea.sh wiki-url` 給的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就整個區塊不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
> HASH:由 `jsc-gitea/tools/hash-id` 對 `{owner}/{repo}` 算出的完整 40 碼大寫十六進位,不截短、不加前綴。
| 計畫名稱 | 計畫頁 | 存取庫 | HASH | 狀態 | 建立時間 | ## PLAN_{HASH}
| --- | --- | --- | --- | --- | --- |
| {計畫名稱} | [[{計畫名稱}|PLAN_{HASH}]] | {owner}/{repo} | {HASH} | 未分析 | {yyyy-MM-dd} | - 計畫名稱:{計畫名稱}
- 計畫頁:[{計畫名稱}](https://{gitea 主機}/{owner}/{repo}/wiki/PLAN_{HASH})
- 存取庫:{owner}/{repo}
- HASH:{HASH}
- 狀態:未分析
- 建立時間:{yyyy-MM-dd}
+3
View File
@@ -1,5 +1,8 @@
# 計畫:{計畫名稱} # 計畫:{計畫名稱}
> 連結寫法:一律寫成 `[{文字}]({連結})`,連結取自 `jsc-gitea/tools/gitea.sh wiki-url` 的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入;有一筆 DEAD 就不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼。
- 頁名:`PLAN_{HASH}` - 頁名:`PLAN_{HASH}`
- HASH:`{HASH}` - HASH:`{HASH}`
- 存取庫:`{owner}/{repo}` - 存取庫:`{owner}/{repo}`
+13 -4
View File
@@ -1,7 +1,16 @@
# 盤點目錄 # 盤點目錄
> 寫入語意:一列代表一個存取庫的盤點。寫入前先讀回整頁,該存取庫已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。 > 存放位置:本頁是目錄頁,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫(沒設定就退回 `JSC_WIKI_REPO`,都沒設定就停)。盤點頁 `REPO_{HASH}` 住在 `JSC_WIKI_REPO_REPO` 那一邊,兩者不同存取庫。
> 版面:一個 H1 頁名、一段 `>` 引言,接著每一筆一個 H2 區塊。H2 標題就是該筆盤點頁的實際頁名(示範區塊寫成 `REPO_{HASH}` 只是佔位符,真正要填的是盤點那一步剛寫出去那一頁的頁名),標題不放連結、不加前後綴。欄位是標題底下的一層條列,一欄一條,格式 `- {欄位名}:{值}`。本頁不留 markdown 表格。
> 寫入語意:一個區塊代表一個存取庫的盤點。一律用 `jsc-gitea/tools/wiki-contents.sh upsert REPO 2 {盤點頁頁名} {區塊檔} {本範本}` 單筆寫回:該存取庫已經有區塊就更新那個區塊,沒有才附加一個區塊,整頁一起送出。禁止整頁覆蓋,也不得改動別人的區塊。參數語意:`2` 是舊表格裡持有內容頁連結那一欄的序號,也就是「盤點頁」欄,只供自動轉檔用——本頁還是舊表格時,腳本從那一欄的連結網址取最後一段路徑當 H2 標題;本頁已經是條列版面就完全忽略它。這個序號填錯,轉出來的標題就跟鍵對不上,既有那一筆會被當成新的附加上去,同一個存取庫的盤點變成兩個區塊,舊區塊從此再也更新不到,所以不是死參數、也不能隨便填。`{盤點頁頁名}` 是 H2 標題,填這一筆對應內容頁的實際頁名,也就是技能自己剛寫的那一頁的頁名,不套 `REPO_{HASH}` 的公式;`{區塊檔}` 是整個 H2 區塊的 markdown(`## {盤點頁頁名}` 那一行加各條欄位),不是列檔。
> 連結寫法:一律寫成 `[{文字}]({連結})`。盤點頁欄位的連結放 `jsc-gitea/tools/gitea.sh wiki-url` 給的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,畫面上看起來像純文字或死連結,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入。有一筆 DEAD 就整個區塊不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404,照狀態碼判會把還活著的頁判成死連結。結束碼 `1`=至少一筆連不到、`2`=一個網址都沒給、`3`=`GITEA_HOST` 未設定、`7`=金鑰失效,停下回報金鑰問題,不得當成連不到。
> HASH:由 `jsc-gitea/tools/hash-id` 對該存取庫自己的 `{owner}/{repo}` 算出的完整 40 碼大寫十六進位,不截短、不加前綴。
| 存取庫 | 盤點頁 | HASH | commit sha | 盤點時間 | ## REPO_{HASH}
| --- | --- | --- | --- | --- |
| {owner}/{repo} | [[{owner}/{repo}|REPO_{HASH}]] | {HASH} | `{sha}` | {yyyy-MM-dd} | - 存取庫:{owner}/{repo}
- 盤點頁:[{owner}/{repo}](https://{gitea 主機}/{owner}/{repo}/wiki/REPO_{HASH})
- HASH:{HASH}
- commit sha:`{sha}`
- 盤點時間:{yyyy-MM-dd}
+3
View File
@@ -1,5 +1,8 @@
# 盤點:{owner}/{repo} # 盤點:{owner}/{repo}
> 連結寫法:一律寫成 `[{文字}]({連結})`,連結取自 `jsc-gitea/tools/gitea.sh wiki-url` 的絕對網址,不自行組路徑,也不用 `[[頁名]]` 或 `[[顯示文字|頁名]]`:後者只在同一個 wiki 內解析,寫錯不會報錯,巡不到也修不了。
> 寫入前驗證:要放進本頁的每一個連結先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫入;有一筆 DEAD 就不寫,把連不到的清單回報給呼叫端。驗證走 API,不看網頁狀態碼。
- 頁名:`REPO_{HASH}` - 頁名:`REPO_{HASH}`
- HASH:`{HASH}` - HASH:`{HASH}`
- commit sha:`{sha}` - commit sha:`{sha}`
+84 -11
View File
@@ -25,12 +25,22 @@
# --target-branch NAME 目標分支(省略時等同來源分支) # --target-branch NAME 目標分支(省略時等同來源分支)
# #
# 輸出: 繁體中文 markdown 階段回報,直接貼給使用者。 # 輸出: 繁體中文 markdown 階段回報,直接貼給使用者。
# 結束碼: 0=回報完整 1=有警告(缺工作日誌,內容已暫存) 2=用法錯誤 3=相依工具找不到 # 結束碼: 0=回報完整 1=有警告(缺工作日誌或有連不到的連結) 2=用法錯誤 3=相依工具找不到
#
# 連結驗證:
# 本檔輸出的是一張給人點的 wiki 連結清單,所以印之前先把這些網址交給
# jsc-gitea/tools/link-check.sh 驗一次,逐列標出「通過、連不到、未驗證」。
# 驗證走 API 不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404。
# 這裡的驗證只註記、不阻擋——頁面在呼叫本檔之前就寫完了,擋下去只會讓使用者連回報都拿不到。
# 呼叫端該做的事在寫入之前:每個要放進頁面的連結先過 link-check.sh,結束碼 0 才寫入,
# 有 DEAD 就不寫並回報。本檔是最後一道複查,不是那一道關卡的替代品。
# #
# 陷阱: # 陷阱:
# - 結束碼 1 是警告,不是阻擋:階段的工作已經做完了,擋下去只會讓使用者拿不到回報。 # - 結束碼 1 是警告,不是阻擋:階段的工作已經做完了,擋下去只會讓使用者拿不到回報。
# - 缺工作日誌時一定要給 --pending-file 與 --log-hash,否則內容留在對話裡,換一個工作階段就沒了。 # - 缺工作日誌時一定要給 --pending-file 與 --log-hash,否則內容留在對話裡,換一個工作階段就沒了。
# - 模型閘門那一列只轉述 sdlc-gate.sh report 的結果,不自己判定標籤。 # - 模型閘門那一列只轉述 sdlc-gate.sh report 的結果,不自己判定標籤。
# - 金鑰失效(link-check.sh 結束碼 7)標成「未驗證」,不標成「連不到」:把金鑰問題寫成死連結,
# 下一手就會照著去刪還活著的頁。
set -u set -u
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
@@ -83,6 +93,13 @@ sdlc_gate_sh() {
resolve_dep hooks hooks/sdlc-gate.sh resolve_dep hooks hooks/sdlc-gate.sh
} }
link_check_sh() {
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/link-check.sh" ]; then
printf '%s\n' "$JSC_GITEA_TOOLS/link-check.sh"; return 0
fi
resolve_dep gitea tools/link-check.sh
}
stage="${1:-}" stage="${1:-}"
case "$stage" in case "$stage" in
plan|analyze|implement|maintain) shift ;; plan|analyze|implement|maintain) shift ;;
@@ -250,6 +267,56 @@ EOF
) )
fi fi
# ---- 連結驗證 ----
# 先把頁名全部解成網址收在 page_rows,再一次餵給 link-check.sh:一頁一次呼叫是白付的往返成本。
TAB=$(printf '\t')
page_rows=''
url_list=''
if [ -n "$pages" ]; then
page_rows=$(printf '%s' "$pages" | while IFS= read -r item; do
[ -n "$item" ] || continue
type=${item%%:*}
page=${item#*:}
url=$(page_url "$type" "$page" || true)
printf '%s%s%s\n' "$page" "$TAB" "${url:-}"
done)
url_list=$(printf '%s\n' "$page_rows" | awk -F"$TAB" 'NF>1 && $2!=""{print $2}')
fi
[ -n "$worklog_cell" ] && [ -n "${wl_url:-}" ] && url_list=$(printf '%s\n%s' "$url_list" "$wl_url")
LINKCHECK=''
link_note=''
dead_urls=''
skip_urls=''
if [ -n "$url_list" ]; then
LINKCHECK=$(link_check_sh || true)
if [ -z "$LINKCHECK" ]; then
link_note='找不到 jsc-gitea/tools/link-check.sh,這次的連結沒有驗證過。'
else
lc_out=$(printf '%s\n' "$url_list" | "$LINKCHECK" 2>/dev/null)
lc_rc=$?
case "$lc_rc" in
0|1)
dead_urls=$(printf '%s\n' "$lc_out" | awk -F"$TAB" '$1=="DEAD"{print $2}')
# SKIP 是「沒有可查的端點」,不影響 link-check.sh 的結束碼,所以分開標、不進警告。
skip_urls=$(printf '%s\n' "$lc_out" | awk -F"$TAB" '$1=="SKIP"{print $2}')
;;
3) link_note='GITEA_HOST 沒設定,連結沒有驗證過;設好變數再跑一次。' ;;
7) link_note='Gitea 金鑰失效(401、403),連結沒有驗證過。這不代表連結是死的,先處理金鑰。' ;;
*) link_note="連結驗證沒跑完(link-check.sh 結束碼 $lc_rc)。" ;;
esac
fi
fi
[ -n "$dead_urls" ] && warn=1
verify_cell() { # $1=網址 -> 驗證結果欄
[ -n "$1" ] || { printf '未驗證'; return 0; }
if [ -z "$LINKCHECK" ] || [ -n "$link_note" ]; then printf '未驗證'; return 0; fi
if printf '%s\n' "$dead_urls" | grep -Fxq -- "$1"; then printf '**連不到**'
elif printf '%s\n' "$skip_urls" | grep -Fxq -- "$1"; then printf '無可查端點'
else printf '通過'; fi
}
# ---- 輸出 ---- # ---- 輸出 ----
printf '## 階段回報:%s\n\n' "$(stage_zh "$stage")" printf '## 階段回報:%s\n\n' "$(stage_zh "$stage")"
printf '| 項目 | 內容 |\n| --- | --- |\n' printf '| 項目 | 內容 |\n| --- | --- |\n'
@@ -257,23 +324,29 @@ printf '| 模型能力標籤 | %s |\n' "$gate_result"
printf '| 工作日誌 | %s |\n' "$worklog_cell" printf '| 工作日誌 | %s |\n' "$worklog_cell"
[ -n "$branch_rows" ] && printf '%s\n' "$branch_rows" [ -n "$branch_rows" ] && printf '%s\n' "$branch_rows"
printf '\n### 寫入的 wiki 頁\n\n' printf '\n### 寫入的 wiki 頁\n\n'
if [ -n "$pages" ]; then if [ -n "$page_rows" ]; then
printf '| 頁面 | 連結 |\n| --- | --- |\n' printf '| 頁面 | 連結 | 連結驗證 |\n| --- | --- | --- |\n'
printf '%s' "$pages" | while IFS= read -r item; do printf '%s\n' "$page_rows" | while IFS="$TAB" read -r page url; do
[ -n "$item" ] || continue [ -n "$page" ] || continue
type=${item%%:*} url=${url:-}
page=${item#*:} printf '| %s | %s | %s |\n' "$page" "${url:-(取不到網址)}" "$(verify_cell "$url")"
url=$(page_url "$type" "$page" || true)
printf '| %s | %s |\n' "$page" "${url:-(取不到網址)}"
done done
else else
printf '本階段沒有寫入任何 wiki 頁。\n' printf '本階段沒有寫入任何 wiki 頁。\n'
fi fi
if [ "$warn" -eq 1 ]; then if [ "$warn" -eq 1 ] || [ -n "$link_note" ]; then
printf '\n### 警告\n\n' printf '\n### 警告\n\n'
[ -n "$worklog" ] || printf -- '- 這個階段還沒有寫入工作日誌,請自行檢查是不是漏了。%s\n' "$pending_note" [ -n "$worklog" ] || printf -- '- 這個階段還沒有寫入工作日誌,請自行檢查是不是漏了。%s\n' "$pending_note"
[ "$stage" = implement ] && [ -z "$pr_url" ] && printf -- '- 目標分支還沒有 PR,工作尚未交出去。\n' [ "$stage" = implement ] && [ -z "$pr_url" ] && printf -- '- 目標分支還沒有 PR,工作尚未交出去。\n'
exit 1 [ -n "$link_note" ] && printf -- '- %s\n' "$link_note"
if [ -n "$dead_urls" ]; then
printf -- '- 下列連結連不到,請修好再貼給別人:\n'
printf '%s\n' "$dead_urls" | while IFS= read -r dead; do
[ -n "$dead" ] || continue
printf -- ' - %s\n' "$dead"
done
fi
[ "$warn" -eq 1 ] && exit 1
fi fi
exit 0 exit 0
+18 -8
View File
@@ -77,6 +77,8 @@
# 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本 # 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本
# 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就 # 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就
# 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。 # 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。
# - --analyze 的頁名原樣送查,本檔不驗 hash 長度:完整 40 碼與還沒搬的舊 8 碼都收得下。
# 驗長度只會在頁名規則變動時把讀得到的頁擋成 missing-dep,判長度的正本在 jsc-gitea。
# - 工作包代號補零與否兩種寫法都會出現(WP-8、WP-08),比對前一律先過 wp_norm 正規化, # - 工作包代號補零與否兩種寫法都會出現(WP-8、WP-08),比對前一律先過 wp_norm 正規化,
# 不然同一包的兩種寫法會被當成兩包,歸屬比對永遠對不上。 # 不然同一包的兩種寫法會被當成兩包,歸屬比對永遠對不上。
# - 領取檔一個存取庫一支,不是一個工作包一支。所以合併後交回領取紀錄之前要先確認 # - 領取檔一個存取庫一支,不是一個工作包一支。所以合併後交回領取紀錄之前要先確認
@@ -329,16 +331,24 @@ case "$sub" in
gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。" gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。"
# 分析頁是內容頁,住在 ANALYZE 型別的存取庫,不是目錄頁那個 CONTENTS 專用存取庫。
# 兩者分家之後這裡最容易被順手改成 CONTENTS:改了就再也讀不到 WBS 表,
# 每個候選工作包都會判成 missing-dep,等於閘門整個停擺。
wiki_repo=$(sh "$gitea" wiki-repo ANALYZE 2>/dev/null) wiki_repo=$(sh "$gitea" wiki-repo ANALYZE 2>/dev/null)
[ -n "$wiki_repo" ] || missing_dep "解析不出 ANALYZE 頁的 wiki 存取庫(JSC_WIKI_REPO_ANALYZE 與 JSC_WIKI_REPO 都未設定)。" [ -n "$wiki_repo" ] || missing_dep "解析不出 ANALYZE 內容頁的 wiki 存取庫(JSC_WIKI_REPO_ANALYZE 與 JSC_WIKI_REPO 都未設定)。目錄頁的 JSC_WIKI_REPO_CONTENTS 不供這裡退讓。"
page="$analyze" page="$analyze"
content=$(sh "$gitea" wiki-get "$wiki_repo" "$page" 2>/dev/null) content=$(sh "$gitea" wiki-get "$wiki_repo" "$page" 2>/dev/null)
[ -n "$content" ] || missing_dep "讀不到分析頁 $wiki_repo 的 $page。" [ -n "$content" ] || missing_dep "讀不到分析頁 $wiki_repo 的 $page。"
# markdown 表格用 \| 跳脫儲存格裡的字面 |(例如描述某個分隔符本身就是 |);
# 下面逐欄用 awk -F'|' 切列,不先把跳脫符換掉,這一格會多出一欄,
# 害同一列後面每一欄全部錯位。先把 \| 換成控制字元,切完欄位再也用不到它,不必換回來。
content=$(printf '%s' "$content" | sed $'s/\\\\|/\x01/g')
# WBS 表欄位(以 | 分隔,$1 是第一個 | 之前的空字串): # WBS 表欄位(以 | 分隔,$1 是第一個 | 之前的空字串,$NF 是結尾 | 之後的空字串):
# $2=編號 $6=相依 $11=PR $12=狀態。 # $2=編號 $7=相依。層 0 比層 1 到 4 多一欄「工作證」,PR 與狀態的絕對欄號因此不同,
# 找列一律比數值,不比字串:範本固定兩位數補零(WP-08),但呼叫端或相依欄裡的寫法 # 一律從結尾往回數:倒數第一格是狀態、倒數第二格是 PR,兩種欄位數的表格都對得上。
# 不一定補零(WP-8);比字串會讓兩種寫法各自找不到對方那一列,誤判成「查不到」。 # 找列一律比數值,不比字串:範本固定把編號補零到兩位數,但呼叫端或相依欄裡的寫法
# 不一定補零;比字串會讓兩種寫法各自找不到對方那一列,誤判成「查不到」。
find_wp_row() { # $1=WP 編號(可帶或不帶前導零) -> 印出該列,找不到印空字串 find_wp_row() { # $1=WP 編號(可帶或不帶前導零) -> 印出該列,找不到印空字串
_n=$(printf '%s' "$1" | sed 's/^0*//'); [ -n "$_n" ] || _n=0 _n=$(printf '%s' "$1" | sed 's/^0*//'); [ -n "$_n" ] || _n=0
printf '%s\n' "$content" | awk -F'|' -v n="$_n" ' printf '%s\n' "$content" | awk -F'|' -v n="$_n" '
@@ -354,7 +364,7 @@ case "$sub" in
[ -n "$row" ] || missing_dep "分析頁 $page 上找不到 WP-$wp_number 這一列,核對不了相依。" [ -n "$row" ] || missing_dep "分析頁 $page 上找不到 WP-$wp_number 這一列,核對不了相依。"
self_wp=$(printf '%s' "$row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }') self_wp=$(printf '%s' "$row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
deps=$(printf '%s' "$row" | awk -F'|' '{ d = $6; gsub(/^[ \t]+|[ \t]+$/, "", d); print d }') deps=$(printf '%s' "$row" | awk -F'|' '{ d = $7; gsub(/^[ \t]+|[ \t]+$/, "", d); print d }')
case "$deps" in case "$deps" in
''|-|無) ''|-|無)
echo 'status=ready' echo 'status=ready'
@@ -387,11 +397,11 @@ case "$sub" in
continue continue
fi fi
dep_wp=$(printf '%s' "$dep_row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }') dep_wp=$(printf '%s' "$dep_row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
dep_status=$(printf '%s' "$dep_row" | awk -F'|' '{ s = $12; gsub(/^[ \t]+|[ \t]+$/, "", s); print s }') dep_status=$(printf '%s' "$dep_row" | awk -F'|' '{ s = $(NF-1); gsub(/^[ \t]+|[ \t]+$/, "", s); print s }')
if [ "$dep_status" = '已完成' ]; then if [ "$dep_status" = '已完成' ]; then
continue continue
fi fi
dep_pr=$(printf '%s' "$dep_row" | awk -F'|' '{ print $11 }') dep_pr=$(printf '%s' "$dep_row" | awk -F'|' '{ print $(NF-2) }')
pr_index=$(printf '%s' "$dep_pr" | sed -n 's#.*/pulls/\([0-9][0-9]*\).*#\1#p' | head -n1) pr_index=$(printf '%s' "$dep_pr" | sed -n 's#.*/pulls/\([0-9][0-9]*\).*#\1#p' | head -n1)
if [ -z "$pr_index" ]; then if [ -z "$pr_index" ]; then
unmet="${unmet}${unmet:+、}${dep_wp}(狀態「${dep_status:-未完成}」,還沒開 PR)" unmet="${unmet}${unmet:+、}${dep_wp}(狀態「${dep_status:-未完成}」,還沒開 PR)"