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
This commit is contained in:
2026-09-02 11:02:34 +08:00
parent d7b3684bb4
commit 9e71474bfb
15 changed files with 194 additions and 95 deletions
+43 -16
View File
@@ -1,6 +1,6 @@
---
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 row (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
@@ -8,13 +8,16 @@ description: SDLC analysis stage. Gate on capability tags enforced in code by sd
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**.
`{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.
**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`. Because directory and content pages sit in different wikis, every directory row links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`; `[[...]]` resolves only inside one wiki and would dead-link from the directory.
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.
## 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.
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. **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.
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.
@@ -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:
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**:
- 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.
- 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.
- 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.
- 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`, then upsert this repository's row in `REPO_CONTENTS` with `jsc-gitea/tools/wiki-contents.sh upsert REPO 1 {owner}/{repo} {row file} templates/repo-contents.md`. The row follows `templates/repo-contents.md`, and the key is column 1, the repository name, written exactly as the row file writes it. Its 盤點頁 cell holds the absolute URL from `gitea.sh wiki-url {REPO repo} REPO_{HASH}` — take that URL first and branch on the exit code per "Every cross-repo link comes from `wiki-url`" below, because the row must never carry an empty link cell.
- Never hand-edit `REPO_CONTENTS`, and never touch a row 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.
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,48 @@ 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.
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.
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.
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.
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. Both directory rows then go through `jsc-gitea/tools/wiki-contents.sh`, never a hand-edited page:
- `jsc-gitea/tools/wiki-contents.sh upsert ANALYZE 3 {HASH} {row file} templates/analyze-contents.md` — the row follows `templates/analyze-contents.md` and the key is the HASH column, column 3. Its 分析頁 cell holds the absolute URL from `gitea.sh wiki-url {ANALYZE repo} ANALYZE_{HASH}`: take that URL after the analysis page is saved and branch on the exit code per "Every cross-repo link comes from `wiki-url`" below, because the row must never carry an empty link cell.
- `jsc-gitea/tools/wiki-contents.sh upsert PLAN 4 {HASH} {row file} templates/plan-contents.md` — rebuild that plan's row from the one the page already holds, change only its status to the literal 「已分析」, and keep every other cell (including the absolute plan-page link) byte-for-byte as it was. The key is the HASH column, column 4.
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 row, both `wiki-contents.sh` runs exited 0, and `ANALYZE_CONTENTS` shows this analysis's row while `PLAN_CONTENTS` 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.
## 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 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 — never a whole-page overwrite, and never a row 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, replaces the row whose key column matches and appends 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 |
| --- | --- |
| 0 | the page is there — upsert this run's row into the content that came back, then write the whole page |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged |
| 0 | the row is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the write failed, or the page holds no markdown table — report it as a failed write and go to step 11 as a failure |
| 2 | an argument was rejected (unknown type, key column, missing row 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 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 row 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 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 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 cross-repo link comes from `wiki-url`
Both directory rows this stage writes — `REPO_CONTENTS` in step 5.2 and `ANALYZE_CONTENTS` in step 10 — link a content page that lives in another wiki repo, so the cell holds the absolute URL `gitea.sh wiki-url {content repo} {page}` printed and nothing else. Run it **after** that content page 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. A row whose link cell 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 row as if it were correct.
Completion condition: every row this stage upserted carries a URL that came out of a `wiki-url` run that exited 0, and every non-zero code was branched on as this table says.
## Delivery package is WP-01
@@ -76,4 +102,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 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 row 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 row 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.
+33 -15
View File
@@ -1,18 +1,23 @@
---
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 row 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
Goal: complete the analysis page's todos one by one; **update the wiki status immediately after every completed item**.
`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. Because directory and content pages sit in different wikis, every directory row links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`; `[[...]]` resolves only inside one wiki and would dead-link from the directory.
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.
## 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.
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. **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.
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.
@@ -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**.
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.
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**:
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`.
@@ -72,12 +77,22 @@ 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**:
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`.
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`, 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 row with `jsc-gitea/tools/wiki-contents.sh upsert DELIVER 6 {HASH} {row file} templates/deliver-contents.md`: the row follows `templates/deliver-contents.md` and the key is the HASH column, column 6, written exactly as the row file writes it. Its 交付頁 cell holds the absolute URL from `gitea.sh wiki-url {DELIVER repo} DELIVER_{HASH}` — 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:
| Exit | What this step does |
| --- | --- |
| 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: a row whose 交付頁 cell is empty names a delivery nobody can open, and the next run replaces that row as if it were correct. 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`.
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.
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.
5. With the delivery produced, ask per `jsc-ask:ask` rules whether to register this project for maintenance: upsert this repository's row with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {row file} templates/maintain-contents.md`, the row built from `templates/maintain-contents.md` and the key column 1, the repository name. 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 交付頁 cell, with any non-zero code branched on as sub-step 12.3 says; **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:
- 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;
- `--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.
@@ -85,20 +100,23 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
## Contents pages are appended, never overwritten
`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.
`DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` are shared directories in the CONTENTS wiki repo: 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 — never a whole-page overwrite, and never a row 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, replaces the row whose key column matches and appends 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 |
| --- | --- |
| 0 | the page is there — upsert this run's row into the content that came back, then write the whole page |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged |
| 0 | the row is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the write failed, or the page holds no markdown table — report it as a failed write and go to step 13 as a failure |
| 2 | an argument was rejected (unknown type, key column, missing row 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 row 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 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.
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 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 and living in the DELIVER repo, so writing it 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.
## Rules
+28 -12
View File
@@ -1,17 +1,30 @@
---
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, which sits 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
Goal: run routine maintenance for every project in the maintenance contents page.
**`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. Any link it carries to a page of another type is the absolute URL from `gitea.sh wiki-url {that type's repo} {page}`, because `[[...]]` resolves only inside one wiki. Branch on that command'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 cell 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 cell that is empty names a page nobody can open, and the next run rewrites that row 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. Step 5 still runs 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 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). 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.
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.
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,26 @@ 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.
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.
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 column 「前次維護時間」) in `MAINTAIN_CONTENTS` to today with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {row file} templates/maintain-contents.md`, never by hand-editing the page. Rebuild that project's row from the one the page already holds, change only the 「前次維護時間」 cell, and keep every other cell byte-for-byte as it was; the key is column 1, the repository name, written exactly as the row file writes it. Branch on the exit code per "Contents pages are appended, never overwritten" below. Completion condition: the script exited 0, `MAINTAIN_CONTENTS` shows today's date in 「前次維護時間」 for that project, and every other project's row 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.
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.
## 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 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 — never a whole-page overwrite, and a project this run did not maintain keeps its row 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, replaces the row whose key column matches and appends 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 |
| --- | --- |
| 0 | the page is there — upsert this project's row into the content that came back, then write the whole page |
| 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 |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged |
| 0 | the row is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the write failed, or the page holds no markdown table — report it as a failed write and go to step 5 as a failure |
| 2 | an argument was rejected (unknown type, key column, missing row 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. `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 row 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.
+31 -14
View File
@@ -1,6 +1,6 @@
---
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 the directory row 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
@@ -8,13 +8,16 @@ description: SDLC planning stage. Gate on capability tags enforced in code by sd
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`).
`{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`. Because the two pages sit in different wikis, the directory row links the plan page by the absolute URL from `gitea.sh wiki-url {PLAN repo} PLAN_{HASH}`: `[[...]]` resolves only inside one wiki and would dead-link from the directory.
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.
## 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` 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. 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.
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.
@@ -25,31 +28,45 @@ 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.
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.
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.
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.
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. 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.
7. Upsert this plan's row 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 row 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: a row whose link cell is empty is a directory entry that points nowhere, and the next run overwrites it as if it were correct. Then build one file holding the single row from `templates/plan-contents.md` — the plan name, that absolute link, the code repository, the HASH, the literal 「未分析」 and the creation date; 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 4 {HASH} {row file} templates/plan-contents.md`. The key is the HASH column, column 4, written exactly as the row file writes it; a key typed by hand appends a second row for the same plan. 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 row, the upsert exited 0, `PLAN_CONTENTS` shows this plan's row with the literal 「未分析」 and that absolute plan-page link, and you have reported both 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.
## 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 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 — never a whole-page overwrite, and never a row 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, replaces the row whose key column matches and appends 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 |
| --- | --- |
| 0 | the page is there — upsert this plan's row into the content that came back, then write the whole page |
| 4 | the page really does not exist yet — this is the **only** code that permits building it from the template |
| 7 | the key is invalid or lacks permission — stop, report the code and its cause, create no page and write nothing |
| 8 | any other API failure — same as 7: stop and report, and do not retry the same call unchanged |
| 0 | the row is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the write failed, or the page holds no markdown table — report it as a failed write and go to step 8 as a failure |
| 2 | an argument was rejected (unknown type, key column, missing row 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' rows 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 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 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
- 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 row 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 row 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.