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
+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.