feat(狀態回報): 收尾寫一筆 skill-end 事件

現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

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

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
This commit is contained in:
2026-09-02 16:01:18 +08:00
parent 0191e7ba8e
commit f38d1f3087
6 changed files with 102 additions and 22 deletions
+16 -1
View File
@@ -19,7 +19,7 @@ Goal: run routine maintenance for every project in the maintenance contents page
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.
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
@@ -42,6 +42,21 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
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. **A row 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 row 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 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 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 row was never written. `MAINTAIN` has no content page, so a row 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