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

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

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

Who
- 本存取庫四支階段技能,以及讀這幾份說明檔決定共識判定與階段回報寫法的流程。
- 稽核與驗證流程改拿新的行為清單比對。
2026-09-02 17:21:10 +08:00

96 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 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, one H2 block per project in the separate CONTENTS wiki repo, then run one sub agent per project: switch to develop or master, propose at least five maintenance actions, commit to a new branch, push, and PR. Write a jsc-log:worklog entry per finished project and update the last-maintained timestamp afterward through jsc-gitea/tools/wiki-contents.sh, then close with tools/stage-report.sh - model tag verdict, worklog link, every wiki link written. Use for periodic upkeep of delivered projects inside their maintenance window; not for projects still mid-implementation or not yet registered in MAINTAIN_CONTENTS.'
---
# maintain
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. **It is a bullet-list directory page, not a table**: one H2 block per project, the heading being that project's own `{owner}/{repo}` — `MAINTAIN` has no content page, so its heading cannot be a page name, and a `MAINTAIN_{HASH}` heading would name a page that does not exist, while `{owner}/{repo}` never drifts and keys just as reliably — and the fields — 存取庫、維護方式、維護起始日、維護截止日、前次維護時間 — a one-level bullet list under it, each written `- {欄位名}:{值}` with a full-width colon. Every link it carries is written as `[{text}]({url})`, and the `{url}` is the absolute URL from `gitea.sh wiki-url {that type's repo} {page}` — one link syntax, whichever wiki the page sits in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below. Branch on `wiki-url`'s exit code every time — this is the same branching `jsc-log:worklog` runs over the same call:
| Exit | What this stage does |
| --- | --- |
| 0 | use the URL it printed, verbatim |
| 4 | that page is not on the wiki — leave the bullet with the literal 「無」 and say so, or, when the page was supposed to have been written by this run, go back and write it before coming here again |
| 5 | the page exists but carries no `html_url` — stop and report it, and never assemble the URL by hand; a hand-built path is not the one Gitea serves |
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4 and never fill 「無」: the page is alive, and 「無」 records a live page as one that does not exist |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL: a bullet that is empty names a page nobody can open, and the next run rewrites that block as if it were correct.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Steps 5 and 6 still run after such a stop.
## Steps
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`, out of the CONTENTS wiki repo (`gitea.sh wiki-repo CONTENTS`, never the MAINTAIN one), and filter projects **still inside their maintenance window**: start date ≤ today, and (end date is NULL or ≥ today). **Read the page as H2 blocks, not table rows**: each `## {owner}/{repo}` heading is one project, and the window dates come from that block's `- 維護起始日:` and `- 維護截止日:` bullets while the repository comes from its `- 存取庫:` bullet. Build every option and every later `upsert` key from the heading of the block the project came out of, never from a hand-computed one. Completion condition: you have listed every in-window project with its `{owner}/{repo}`, its H2 heading and its window dates, or reported that none is in window and stopped.
3. **Align every project in one batch first, then run one sub agent per project.** Completion condition: every project listed in step 2 has its sub agent finished, and each one ends in either a PR link or a recorded skip reason.
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:
- dependency updates (reuse `jsc-pkg:pkg-update`)
- security vulnerability scan and patching
- dead code and stale comment cleanup
- test coverage reinforcement
- docs and README synchronization
- build warning elimination
Completion condition: the user has picked the methods to apply, and every picked method is either applied or reported with the reason it could not be.
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 bullet 「前次維護時間」) in `MAINTAIN_CONTENTS` to today with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {entry file} templates/maintain-contents.md`, never by hand-editing the page. Rebuild that project's H2 block from the one the page already holds: keep the same `## {owner}/{repo}` heading, change only the `- 前次維護時間:` bullet, and keep every other bullet byte-for-byte as it was. **The third argument is that H2 heading — for `MAINTAIN` it is the project's own `{owner}/{repo}`**, taken from the block step 2 read, matching the entry file's own heading byte for byte; never substitute a `MAINTAIN_{HASH}` key, which would point at a page that does not exist. The `1` is the column number of the old table column that carries this entry's identity — the 存取庫 column, the only column `MAINTAIN` has that identifies a row, and it holds no link, so the conversion takes its plain text. It is used only for the automatic conversion: while the page on the wiki is still a markdown table the script reads the heading out of that column, and once the page is bullet-list shaped the number is ignored. **A wrong number is not harmless**: the heading it converts to will not match the key, this project's existing entry gets appended as a new one, one project ends up with two blocks, and the old block is never updated again. **A block that carries a link goes through `jsc-gitea/tools/link-check.sh` before the upsert, and is upserted only on exit 0** — see "Every link is checked before it reaches a page" below. Branch on the upsert's exit code per "Contents pages are appended, never overwritten" below. Completion condition: the script exited 0, every link in the rebuilt block was cleared by a `link-check.sh` run that exited 0, `MAINTAIN_CONTENTS` shows today's date in 「前次維護時間」 for that project, and every other project's block is byte-for-byte unchanged.
4. The main agent reports the summary: maintenance methods applied per project, PR table rows, and failure reasons. The report and all generated wiki content, commits, and PR descriptions stay Traditional Chinese per the STE100 rule. Completion condition: the summary names every project read in step 2, each with its applied methods and either a PR table row or the reason it was skipped.
5. **Stage report — the last thing this stage does, including when no project was in window, and when a wiki read or write failed.** Run `tools/stage-report.sh maintain` with one `--page TYPE:{page}` per wiki page this run wrote — that is `--page CONTENTS:MAINTAIN_CONTENTS`, under the `CONTENTS` type, because the script resolves each page's repo from the TYPE you pass and `MAINTAIN:` would resolve the wrong repo and print no URL — plus `--worklog` and `--worklog-heading` pointing at the entries step 3.5 wrote. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any project finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log. Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and every wiki page this run wrote appears in it.
6. **Write this run's `skill-end` status event — the very last thing this stage does, right after step 5, on every path including when no project was in window.** Run `jsc-hooks/tools/report-status.sh skill-end jsc-sdlc:maintain {status} {exit} [detail]`, naming the script the way this stage already names `jsc-hooks/hooks/sdlc-gate.sh` in step 1. The matching `skill-start` event is written by jsc-hooks on its own, so this step owes only the `end`: a hook fires on the skill tool call and this stage's work happens in the model turns after it, so **no hook can see how this run ended**. A `start` with no `end` is what an aborted run looks like in the record, and a stage that often ends with nothing to do needs that difference recorded, not guessed.
`{status}` is one of five words, never a sixth:
| Status | When `maintain` reports it |
| --- | --- |
| `ok` | Every step's completion condition is met: the gate passed, every in-window project ran its sub agent and ended in a PR, each finished project has its work log entry, `MAINTAIN_CONTENTS` shows today in 「前次維護時間」 for every one of them, and `tools/stage-report.sh` exited 0 |
| `blocked` | A check that lives in code stopped the run before any maintenance: `sdlc-gate.sh lock maintain` exited non-zero because the script could not determine the actual model id from the transcript, which is the one thing this stage's gate asks for; or step 3.1 found every in-window project out of step with `origin/{branch}`, so all of them were skipped and not one maintenance action ran. Nothing was maintained, so this is **never `failed`** — both are the guard working |
| `failed` | Maintenance ran and then a write did not land: `wiki-contents.sh` returned 1, 7 or 8 over `MAINTAIN_CONTENTS`, `wiki-url` returned 5, 7 or 8, or `link-check.sh` returned 1 so the block was never written. `MAINTAIN` has no content page, so a block that never lands loses the whole wiki record of this run — that is why it is `failed` and not `degraded` |
| `degraded` | Some projects came through and some did not: one project was skipped for a branch gap or a method that could not be applied while the others got their PR, or every project got its PR while `wiki-contents.sh` returned 3 so no 「前次維護時間」 was updated, or `tools/stage-report.sh` exited 1 (no work log, or a link in its list does not answer) |
| `aborted` | The user stopped the run, or the run stopped itself because its premise did not hold — step 2 found no project inside its maintenance window, so there was nothing to maintain |
`{exit}` is the exit code of the script whose verdict decided the status — the gate's code for `blocked`, the failing script's code for `failed` and `degraded` — and `0` when nothing exited non-zero, `ok` and `aborted` included. `[detail]` is optional and Traditional Chinese per the STE100 rule: one line, no line break, naming what decided the status (for example 「無專案在維護期內」 or 「兩個專案與遠端有落差已略過」). The script truncates it at 200 characters, so put the short reason there and nothing else.
**A failure in this step never changes this stage's verdict.** The script is not found (jsc-hooks is not installed on this machine, or this CLI's layout puts it somewhere else) → skip the event quietly and carry on; nothing is reported to the user and no step is re-run. The three recording sub-commands are built to exit 0 even when the write fails, so a non-zero code here means only that the call itself was malformed (exit 2, a usage error) — fix the arguments once and, either way, never turn a stage that opened its PRs into a failed one because the record of it failed. Completion condition: one `skill-end` event has been written for this run, or the script could not be found and that skip is the reason no event exists.
## Every link is checked before it reaches a page
**One syntax.** Every link this stage writes — in the `MAINTAIN_CONTENTS` entry block, in a PR description, in the stage report — is written as `[{text}]({url})`. The `{url}` is the absolute URL `gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
**Checked before it is written.** Collect every link the entry block is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this stage does |
| --- | --- |
| 0 | every link answers — upsert the entry block |
| 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 2 | usage error: not one URL was passed — pass the links and run it again |
| 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and `MAINTAIN_CONTENTS` is the whole record of this stage, so a block rewritten on that reading loses links nothing else holds |
Completion condition: every entry block this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code was branched on as this table says.
## Contents pages are appended, never overwritten
`MAINTAIN_CONTENTS` is a shared directory in the CONTENTS wiki repo: every H2 block on it belongs to somebody's project, and this run reads none of those blocks from anywhere else. So step 3.6 is an upsert of one block — never a whole-page overwrite, and a project this run did not maintain keeps its block untouched. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, converts a page still holding a markdown table into blocks first, replaces the block whose H2 heading matches the key and appends at the end when none matches, then writes the page back.
Branch on its exit code:
| Exit | What this step does |
| --- | --- |
| 0 | the block is in place — carry the `updated` or `added` word it printed into the stage report |
| 1 | the page content could not be assembled, or the write failed — report it as a failed write and go to step 5 as a failure. **A page holding no matching block is not this code**: with nothing to replace the script appends the block and exits 0 |
| 2 | an argument was rejected (unknown type, bad key-column number, missing entry file) — fix the argument and run it again; nothing was written |
| 3 | no CONTENTS wiki repo is configured — stop and report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set. `MAINTAIN` has no content page, so nothing of this stage's record survives elsewhere: the PR is open but the maintenance date is unrecorded, and it is reported that way |
| 4 | the directory page is absent and no template was passed. **Step 3.6 always passes `templates/maintain-contents.md`, so this code does not come out of this skill's call** — a wrong template path is rejected as 2, not as 4. Seeing it anyway means the template file is not where the plugin puts it: confirm the plugin installation is complete and run it again, and never answer it by dropping the template argument. An absent page also means step 2 had no project to maintain |
| 7 | the key is invalid or lacks permission — stop and report the key problem; the script wrote nothing, which is what keeps every other project's block alive |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Why 7 and 8 abort: both mean the old content is unknown, not that the page is missing. Reading either as "not there yet" 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-contents.sh` exit code it branched on, and the directory page was created on no code other than 4.