Files
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

107 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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, 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
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**.
`{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.
**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
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`, 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**. **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:
- Goal: the problem to solve and the criteria for success.
- Scope: what is included, what is excluded, which repositories are involved.
- Feasibility: whether the system architecture and data sources support the goal.
**One round is never enough**: every answer must produce the next question, derived from what that answer just exposed. Never fill a gap with your own assumption, and never move on to user stories while any item is still open.
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.
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 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:
| 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
`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.
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 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 |
| 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. 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" 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-contents.sh` exit code it branched on, and no directory page was created on any code other than 4.
## Hard limits
- 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.
- **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 stop questioning after one round; consensus is reached only under `references/consensus.md`, and the user says so.
## What the work package gate no longer stops here
`jsc-hooks/hooks/sdlc-gate.sh wp-check` used to block this skill outright while the repository still held an unsettled work package PR. For `plan` it is now a reminder that prints and lets the run through. The protection given up is the **work-in-progress cap**: nothing stops a new plan from starting while packages from the last one are still open, so plans can pile up faster than they are implemented. The reminder still names the unsettled package, and the same gate still blocks `analyze` and `maintain`, so the cap holds one stage later. `implement` was always waved through — that is the path that settles the open PR, and a gate that blocked it would lock itself.