From 044676b1fc17394efeda3f4456e8313d563fde22 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 4 Sep 2026 14:41:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(schedule):=20=E6=8E=92=E7=A8=8B=E6=A2=9D?= =?UTF-8?q?=E7=9B=AE=E6=8A=8A=20PATH=20=E4=B8=80=E8=B5=B7=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E9=80=B2=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cron 給的 PATH 只有 /usr/bin 與 /bin,而這台機器的 CLI 執行檔裝在家目錄底下。 於是無人值守那一輪偵測 CLI 一支都找不到——而且是回結束碼 0 加一份空清單, 不是失敗。看起來像「這台機器沒裝 CLI」,實際上是 PATH 上找不到。 實測踩到:那一輪印出 clis=0,帶 CLI 代入點的內建項全部代不出目標而被擋下, 同一支腳本在有人的 shell 裡回五支。 修在條目這一層,不是讓偵測那一支自己去猜安裝路徑:猜就要維護一份常見路徑 清單,而清單會過期,過期的方式跟今天修掉的那幾個一模一樣。條目本來就快照 十個環境變數,多這一個等於把「這台機器的執行檔裝在哪」交給安裝當下的環境 回答,不必任何人維護。 巡檢那一輪要跑的指令本身走的是字面絕對路徑、不靠 PATH;靠 PATH 的是那幾支 腳本自己呼叫的外部程式,所以這一條補的是後者。 實測:拿條目會帶的那條 PATH 在清空環境裡跑,五支 CLI 全解得出來;對照組用 cron 預設的 PATH,零支。 三份 manifest 版號 0.2.4 升到 0.2.5。 Co-Authored-By: Claude Opus 5 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- references/behaviors.md | 2 +- tools/schedule.sh | 10 +++++++++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 23e3c38..4081e3e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-assist", - "version": "0.2.4", + "version": "0.2.5", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index eeb25c8..5ef305e 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-assist", - "version": "0.2.4", + "version": "0.2.5", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "skills": "./skills", "jsc": { diff --git a/plugin.json b/plugin.json index 65772c5..8f5ffba 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-assist", - "version": "0.2.4", + "version": "0.2.5", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "skills": "./skills/", "jsc": { diff --git a/references/behaviors.md b/references/behaviors.md index 3c9e2fb..7d36574 100644 --- a/references/behaviors.md +++ b/references/behaviors.md @@ -8,6 +8,6 @@ | --- | --- | | 觸發時機 | 要啟動助理、要停止助理、要跑一輪巡檢,或要問助理現在還在不在跑、待辦簿剩下哪幾筆時用。四個操作 `start`、`status`、`patrol`、`stop` 都走這一支。排程每一輪叫起來的也是這一支的 `patrol`。委派清單改過之後要讓助理的內建定期檢查項跟著重建,也走這一支的 `start`;只想知道差在哪、不要動待辦簿就走 `status`。執行環境健檢不走這支,走 `jsc-cli:doctor`。技能使用次數不走這支,走 `jsc-log:stats` | | 關鍵步驟 | 四個操作都先跑同一個前置步驟,取得工具根目錄(本頁記成 `{CURRENT}`),根目錄一律由外面餵進來:排程那一輪從叫用文字裡的「工具根目錄=」那一段取字面絕對路徑,一個指令都不跑;人在現場叫用時,叫用文字帶那一段就取那一段,沒帶才跑一次 `readlink -f "${JSC_HOME:-$HOME/.jsc}/current"` 自己解,那一次會跳一次權限詢問,人按一下就過。無人值守那一輪取不到根目錄就停下回報:說明條目是舊版 `schedule.sh` 裝的、沒有把根目錄寫進提示文字,叫人重跑一次 `start` 或 `schedule.sh install patrol` 把條目重寫,收尾狀態取 `aborted`;一律不跑 `readlink`、不跑 `ls`、不退回帶變數的路徑、不拿技能提示或上一次轉錄裡的路徑、也不猜。整次叫用只取這一次,之後每一次腳本呼叫都填那一個字面絕對路徑,不是每一次呼叫各取一次,也不另外加印路徑的工具,更不另外跑指令去驗那一個路徑。取到的是空的、不是絕對路徑、或那條路徑不是存在的目錄,就回報根目錄不見了、叫人跑 `jsc-cli:deploy`,收尾狀態取 `aborted`;第四項要單獨查:解析式帶了預設值之後,`JSC_HOME` 沒設不再解成 `/current`,但 `JSC_HOME` 指到已經不存在的目錄、`HOME` 在精簡環境裡沒設、連結農場根本還沒部署,三種都一樣會交出一條非空又絕對、前三項全過得了關的路徑,所以人在現場那一次要在同一步再跑 `[ -d "{剛印出來的路徑}" ]`,目錄存在才算取到根目錄;排程那一輪不查,它的根目錄是裝排程的人寫進條目的,根目錄不對就會在第一支腳本呼叫上失敗。除了人在現場那一次 `readlink`,任何指令列都不得出現 `$JSC_HOME`、`${JSC_HOME}` 或 `~`:權限層比對的是還沒展開的指令字面。實測歸納出兩條判準:一、無人值守時只有允許清單上的完整字面指令跑得動,沒有「預設安全的唯讀指令」這回事,連 `readlink -f "$JSC_HOME/current"`、`ls -d "$JSC_HOME/current"` 與沒有規則的 `ls -d /root/.jsc/current` 都被擋;二、路徑中段的萬用字元不匹配,版本號寫成 `*` 的快取路徑規則一樣擋,規則與指令都必須是完整字面。排程那一輪沒有人可以按同意,被擋就是停在第一支腳本,什麼都不記,心跳也寫不出來。接著認出使用者要的是哪一個操作,`patrol` 那一路全程不問人。`start`:先跑 `tools/seed-tasks.sh apply --root {CURRENT}` 把內建定期檢查項對齊委派清單(清單在 `{CURRENT}/jsc-meta/tools/delegate-spec.tsv`,根目錄一律用 `--root` 餵進去、那一支自己不解),清單上可交而待辦簿沒有的就加一筆、待辦簿有而清單上已經沒有或改成不交的就移除、`origin` 是 `user` 的一律不動、判定是 `cond` 的預設保留不種入並印出條件原文、`trigger`、`recur` 或 `action` 變了只印 `drift=` 不改那一筆(上游把 `probe` 那一欄合併進來的那一輪,四筆指令型就是走這一條,要換值得人親自帶 `--refresh`);種入與重建是同一個呼叫、冪等,所以每一次 `start` 都跑,第二次跑不會重複建;結束碼 1、2、3 都是「一筆都沒動」,不中止啟動,照實記進收尾回報再往下走,4 是部分失敗、成功的那幾筆算數,一律不帶 `--force` 也不自己帶 `--allow-cond`;接著照 `patrol` 的每一步跑完一輪巡檢,第一次心跳由那一輪寫、不另外寫、跑不完就不算啟動、跑 `heartbeat.sh report` 確認 `state=fresh`、跑 `tools/schedule.sh install patrol` 裝巡檢那一筆排程、把它印的 `allow_rule=` 每一行、`patrol_root=`(條目寫進去的字面根目錄,之後每一輪都從那裡讀)、環境快照提醒與 `current` 連結缺漏的警告原樣轉給人、依結束碼選一段收尾訊息印出——排程接上、排程寫進去了但 cron 沒在跑、排程沒接上三種各一段。心跳那一筆不裝了,`install heartbeat` 一律回 6。`patrol`:跑 `tools/patrol.sh collect` 取鎖並讀五項來源(那一輪另外會自己叫一次 `tools/due.sh scan`,把待辦簿的事件偵測與到期判定寫成「待辦簿到期與逾期」那一節,技能本文一律不自己再叫一次——`scan` 會推進事件快照,同一輪叫第二次就比不出任何事件,而那一次會回報零事件、看起來完全正常;要看下一輪會判出什麼就叫 `due.sh events`,那個子命令一律唯讀)(第五項是執行狀態事件:`collect` 自己叫 `jsc-hooks/tools/report-status.sh drain` 排空,緊接著跑 `rotate`,再把非 ok 的事件與「有 start 沒有配對 end」的技能彙整成監控頁那一節;技能本文一律不自己再跑一次 `drain`)、結束碼 4 就讓開不寫任何東西、結束碼 1 與 3 照樣把這一輪寫上監控頁、`hash` 是空的就 `abort`、經 `jsc-gitea:wiki` 讀回 `MONITOR_{HASH}` 舊頁、基本資料原樣留著、最新一輪那一塊整塊換成 `latest_file`、`summary_file` 的本輪那一列擺最上面(五欄:巡檢時間、本輪判定、各項成敗、待人處理、警示來源)、舊的資料列接在下面並截到 24 列、三塊重組成整頁、寫回之前先把這一頁要放進去的每一個連結交給 `jsc-gitea/tools/link-check.sh`(結束碼 0 才整頁寫回,結束碼 1 就把 DEAD 那幾筆原樣回報並 `abort`,2、3、7 同樣 `abort`,一個連結都沒有就跳過這一次驗證並照實說明)、頁不存在(唯有結束碼 4)才用 `newpage_file` 建頁、讀不回舊頁就不寫、監控頁寫成之後跑 `gitea.sh wiki-url` 取那一頁的絕對網址並依結束碼分流(4 回步驟三重寫、5 沒有 `html_url`、7 與 8 走 `abort`,其餘非 0 也走 `abort`,網址取不到就不寫那一個區塊)、換掉 `contents_file` 那個 H2 區塊裡 `{監控頁絕對網址}` 那個佔位、換完再用 `link-check.sh` 驗那一個網址(結束碼 0 才寫那一個區塊;非 0 一律不寫,比照目錄頁結束碼 3 當成那一個區塊沒更新、這一輪照樣往下寫心跳,並把連不到的那一筆列進待人處理)、用 `jsc-gitea/tools/wiki-contents.sh upsert MONITOR 1 "MONITOR_{HASH}" {區塊檔}` 以 H2 標題(也就是內容頁頁名,取 `collect` 印的 `page=`)當鍵更新 `MONITOR_CONTENTS` 自己那一個區塊並一律帶上 `templates/monitor-contents.md` 當範本(第三個參數 `1` 是 `key-col`,只在舊頁還是 markdown 表格時用得到:舊表格第 1 欄「監控頁」持有身分,那一格是 `[MONITOR_{HASH}](網址)`,轉檔時只取文字當標題;頁面已經是條列格式時這個參數被忽略,照樣固定給 `1`)、目錄頁回 3(`CONTENTS` 存取庫沒設定)不中止這一輪,照樣往下寫心跳,並把「設 `JSC_WIKI_REPO_CONTENTS` 或 `JSC_WIKI_REPO`」列進待人處理、監控頁任一失敗或目錄頁其餘非 0 才 `abort` 且不寫心跳、跑 `tools/patrol.sh finish` 寫心跳、最後印出各項結果、本輪事件數與非 ok 事件數、非 ok 事件的明細(kind、name、status、exit、detail)、以及有 start 沒有配對 end 的那幾支技能(單獨列,那代表那一輪中止了)、兩次寫入各自的連結驗證結果(通過、無連結而跳過、或被擋下並附結束碼與 DEAD 明細)、判成警示時的警示來源與待人處理列。`status`:跑 `heartbeat.sh report` 取心跳現況、把 `state` 對映成新鮮、過期、心跳檔損壞、不存在、不自己解析心跳檔也不自己判定、從 `file=` 解出助理目錄後列出 `tasks/` 底下每一個檔案並解析 `state`、`title`、`next_run`、`fail_count`、跑 `tools/schedule.sh status` 取排程現況與週期、印成心跳、排程、待辦三塊、`fail_count` 大於 0 的列標上「已連續失敗 N 次」、心跳與排程兜起來會誤讀的四種組合各補一句話、最後跑 `tools/seed-tasks.sh plan --root {CURRENT}` 唯讀比對內建項與委派清單並印出差在哪(該加幾筆、還剩幾筆孤兒、保留的 `cond` 各是哪一支、`drift=` 各要換什麼值),一律不跑 `apply`,並說明要套用差異就跑 `start`;那一支回 1、2、3 就照實說比不出來、不說成已對齊。`stop`:先跑 `heartbeat.sh report` 留下原本的狀態、再跑 `tools/schedule.sh remove all` 移除排程與舊版遺留的心跳條目、最後才跑 `heartbeat.sh clear` 清掉心跳、印出停止訊息並說明心跳清掉之後閘門會擋人、同時說明閘門還沒接線所以現在擋不到人。四個操作最後都一樣:回報印完之後跑一次 `jsc-hooks/tools/report-status.sh skill-end jsc-assist:assistant {status} {結束碼}`,`start` 由 hook 記、`end` 由這裡寫,不寫就等於這一次自己看起來中止了 | -| 外部呼叫 | 工具一律走前置步驟取得的根目錄底下那一組不帶版本的路徑(本頁記成 `{CURRENT}`,實際填的是像 `/root/.jsc/current` 這種字面絕對路徑):`{CURRENT}/jsc-assist/tools/patrol.sh`、`{CURRENT}/jsc-assist/tools/schedule.sh`、`{CURRENT}/jsc-assist/tools/due.sh`、`{CURRENT}/jsc-hooks/hooks/heartbeat.sh`,wiki 那一支是 `{CURRENT}/jsc-gitea/tools/gitea.sh`,目錄頁那一支是 `{CURRENT}/jsc-gitea/tools/wiki-contents.sh`,連結驗證那一支是 `{CURRENT}/jsc-gitea/tools/link-check.sh`,執行狀態事件那一支是 `{CURRENT}/jsc-hooks/tools/report-status.sh`,範本是 `{CURRENT}/jsc-assist/templates/monitor-contents.md`;`JSC_HOME` 沒設時,人在現場那一次 `readlink` 自己退回 `~/.jsc` 再解,排程那一輪則直接用條目餵進來的值,指令列上不留變數也不留波浪號;不拿技能提示給的快取基底目錄組工具路徑——權限只放行 current 那一組,快取路徑帶版本號,規則寫成萬用字元也對不上,用錯路徑會被靜靜擋掉。`jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。`write` 只由 `tools/patrol.sh finish` 呼叫,技能自己不呼叫。本 domain 的 `tools/schedule.sh` 的 `install`、`remove`、`status` 三個子命令:`install` 會查 `{CURRENT}/jsc-assist` 與 `{CURRENT}/jsc-gitea` 兩個連結在不在、不在就警告且不代建,會把巡檢的 CLI 用 `command -v` 解成絕對路徑、把 `GITEA_HOST`、`GITEA_TOKEN`、`JSC_HOME`、`JSC_ASSISTANT_HEARTBEAT_TTL` 與所有已設定的 `JSC_WIKI_REPO` 系列快照進條目(含內容頁的 `JSC_WIKI_REPO_MONITOR` 與目錄頁的 `JSC_WIKI_REPO_CONTENTS`,名單當下從環境撈、不寫死,新頁型自動涵蓋)、條目自帶 `JSC_GITEA_CONFIRM=yes`、刻意不寫死工作階段代號(寫死會讓那一輪的 `start` 與 `end` 落在不同的代號上:前者由 hook 從標準輸入的 JSON 讀 CLI 真正的代號,後者由工具腳本只讀得到環境變數,兩半永遠配不起來,每一輪都被算成一支中止的技能)、把自己解好的字面根目錄寫進條目的提示文字(固定格式 `工具根目錄={字面絕對路徑}`,那一輪就是從這裡讀根目錄)並印成 `patrol_root=`、`--patrol-cmd` 或 `JSC_ASSIST_PATROL_CMD` 給的自訂指令沒帶那一段時只警告不中止、並印出這一輪要開的 `allow_rule=` 規則(七支腳本各三種呼叫形式,含 `gitea.sh`、`wiki-contents.sh`、`link-check.sh` 與 `jsc-hooks/tools/report-status.sh`——`Skill(jsc-gitea:wiki)` 只放行叫用技能,技能內部的 Bash 呼叫仍各自受檢;路徑是 `current` 那一組確切路徑,不用萬用字元);七個結束碼各有處置:0 往下走、1 是條目裝了但 cron 沒在跑要照實講不會執行、2 是缺 jsc-hooks 導致門檻讀不到、3 是這台機器沒有排程機制、4 是排程操作失敗要原樣引用 stderr、5 是回讀驗證失敗要叫人自己去看 `crontab -l`、6 是呼叫寫錯,含 `install heartbeat`、週期塞不進門檻、判不出 CLI、那一支 CLI 的執行檔不在 `PATH` 上,以及 `JSC_HOME` 解不出絕對路徑(條目寫不出字面根目錄)。本 domain 的 `tools/patrol.sh` 的 `collect`、`finish`、`abort` 三個子命令,七個結束碼各有處置:0 往下走、1 部分失敗照樣寫頁、2 是 finish 找不到 heartbeat.sh 要回報「記下來了但沒有心跳」、3 是各項全失敗照樣寫頁且判定異常、4 是讓開或鎖被搶走一律不寫心跳、5 是檔案系統失敗要吵出來、6 是呼叫寫錯。巡檢那五項讀 `jsc-log/tools/usage-stats.sh`、`jsc-hooks/hooks/version-guard.sh report`、`jsc-hooks/hooks/restart-gate.sh report`、`$JSC_HOME/sessions/*.stage`、`$JSC_HOME/wp/*.pr`、`heartbeat.sh report`、`jsc-hooks/tools/report-status.sh drain` 與 `rotate`,除了排空會把事件流的位移往前推之外全部只讀,任一項失敗不影響其餘各項。`report-status.sh` 三個結束碼各有處置:0 是排空到新事件、3 是沒有新事件(正常狀態,不是失敗)、2 是呼叫寫錯;找不到這一支、`drain` 回 0 與 3 以外的碼、或 `rotate` 回非 0,都只讓這一項標成失敗或記一筆警示,一律不中止那一輪——回報鏈自己壞掉不可以把被回報的那一輪拖下去。`rotate` 只在 `drain` 成功時緊接著跑:中間隔越久,那段時間新寫進來的事件被搬進備份檔而從此排不到的機會越大;排空失敗時位移狀態未知,這時候輪替會直接吃掉還沒排空的那一批。配對以 `session` 加 `name` 為鍵,不只看 `name`:五支 CLI 併發時同一支技能會有好幾個工作階段同時在跑。沒配對到的 `start` 留在 `$JSC_HOME/assistant/events-open.tsv` 跨輪繼續配對,開超過心跳門檻才算疑似中止,未達門檻的算還在跑,超過一天沒配對到就丟掉。wiki 讀寫一律經 `jsc-gitea:wiki`,技能自己不拼 API 呼叫;只有目錄頁那一個 H2 區塊例外,走 `jsc-gitea/tools/wiki-contents.sh upsert`,它自己解 `CONTENTS` 存取庫、自己讀回整頁比對標題,舊頁還是 markdown 表格時自己先整頁轉成 H2 區塊再寫,七個結束碼各有處置:0 已更新或已新增、1 組不出頁面內容或寫入失敗要 `abort`(找不到同名標題不算錯,那是附加)、2 參數錯就改正重跑(範本路徑不存在也回這一碼,代表 plugin 沒裝齊)、3 是 `CONTENTS` 存取庫未設定且**不中止這一輪**、4 是頁不存在又沒給範本,本技能一律帶第五個參數所以不會出現、7 金鑰失效要 `abort`、8 其他 API 失敗要 `abort`。比對鍵取 H2 標題,也就是內容頁頁名 `MONITOR_{HASH}`,不取「監控頁」那一條的連結:連結含 `GITEA_HOST` 與頁名的網址編碼,那三樣一變鍵就對不上,同一台機器每輪多附一個區塊;頁名只由 `{主機名}/{登入帳號}` 決定,那三樣都動不到它。連結一律寫成 `[{文字}]({絕對網址})`,網址只取 `gitea.sh wiki-url` 印的那一個、不自己組路徑,那一支的結束碼 4、5、7、8 與其餘非 0 各有處置;每一個要放進頁面的連結在寫入前先過 `jsc-gitea/tools/link-check.sh`,它每個網址印一行 `{OK|DEAD|SKIP}` 加網址加說明,五個結束碼各有處置:0 才准寫入、1 有連不到的就不寫並回報 DEAD 那幾筆、2 是一個網址都沒給要補參數重跑、3 是 `GITEA_HOST` 未設定要先設定且不得跳過驗證、7 是金鑰失效要停下來回報金鑰問題而不是當成死連結;驗證走 API 不看網頁狀態碼,私有存取庫的網頁網址對未登入請求一律回 404。頁名雜湊一律取 `gitea.sh hash-id`/`tools/hash-id` 印的完整 40 碼大寫十六進位,不截短、不加前綴、不手算,空輸入回 2。crontab 與 schtasks 一律經 `tools/schedule.sh`。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。待辦簿的存放格式與讀寫入口是本 domain 的 `tools/tasks.sh`:一筆一檔、純文字 key=value、十五個鍵順序固定、值是空的照樣寫出那一行,讀的時候只在第一個等號斷開,寫的時候把值折成一行,一筆一檔的理由同 `restart-required.d`(並行寫入不互相覆寫),`id` 取共用 hash 規則那四十碼的前 8 碼、碰撞時每次加長兩碼,七個子命令 `list`、`add`、`done`、`fail`、`pause`、`resume`、`remove` 與八個結束碼的完整說明寫在那一支的檔頭;第十五個鍵是 `spec_key`,值是 `jsc-{domain}:{技能名}`,那是從一支技能反查到它對應那一筆內建項的唯一把手(`id` 是建立時間加標題的雜湊、反查不了;標題與 `action` 拿來當鍵會撞上使用者交辦的那幾筆),只有 `origin=assistant` 帶得上它,`--spec-key` 配 `--origin user` 回 2,`remove` 對 `origin=user` 的那一筆一律回 7、除非人親自帶 `--force`。內建定期檢查項照委派清單種入與重建的入口是本 domain 的 `tools/seed-tasks.sh`,兩個子命令 `plan`(唯讀預覽)與 `apply`(真的做),清單路徑取 `--root` 餵進來的那一個字面絕對路徑底下的 `jsc-meta/tools/delegate-spec.tsv`;欄位對映是 `trigger` 與 `recur` 原樣抄、`spec_key` 由清單前兩欄合成、`action` 由 `way` 與第十二欄 `probe` 一起推、`title` 固定寫成「委派清單內建項:{spec_key}」以免清單一改就換 `id`、`kind` 一律 `check`、`origin` 一律 `assistant`、`repo` 與 `due` 一律留空,清單的 `verdict`、`slice`、`human`、`next`、`version` 與它自己的 `origin` 欄一律不抄(清單的 `origin` 是 `seed` 或 `judged`,與待辦簿的 `origin` 同名不同義);`action` 那一欄的推法分兩路:`way` 含 `invoke` 就取技能名、`probe` 連看都不看(填了指令會讓整支交出變成只跑一支腳本,那支技能該寫的頁一頁都不會寫,所以那是清單填錯,照 `way` 取技能名並印一行 `probe_bad=`),其餘那幾種交出方式照 `probe` 走——一行指令就取那一行指令、`pending:{理由}` 取 `remind` 並印一行 `pending=`、減號或空的取 `remind`;`probe` 代不進去一律退回 `remind` 並印一行 `probe_bad=`,照樣種入那一筆,涵蓋路徑不是 `{root}/jsc-{domain}/` 開頭、指令裡有金錢符號或波浪號、出現三個代入點以外的大括號、代不出根目錄、代出來的腳本不在這台機器上五種(不種入等於讓上游一格填錯把一筆帶著 `last_run` 與 `fail_count` 的內建項刪掉);`{root}` 由這一支代成 `--root` 給的字面絕對根目錄(沒給就從清單位置往上推三層),那一層的目錄名以 `jsc-{domain}` 為準、找不到才退回不帶前綴的 `{domain}`,而 `{cli}` 與 `{repo}` 刻意留在值裡不展開——那兩件事種入的當下還不知道,種入時展開成多筆會讓同一個 `spec_key` 有好幾個檔案、一致化每一輪只印 `dup=`,而且 CLI 或存取庫一變就要移除再重新登錄、歷史跟著歸零;**`action` 裡出現大括號就是還沒代好的代入點,任何讀取端一律不得原樣拿去執行**,展開由往後接上來的執行那一步負責,`{cli}` 換成每一支偵測到的 CLI 代號、`{repo}` 換成每一個掃到的存取庫工作目錄,一個目標跑一次,所有目標的結果合起來算這一筆的一次成敗;`pending` 的那幾筆只印在回報裡、待辦檔上一個字都不加(寫進標題會換 `id` 又比不出漂移,另立欄位要動待辦簿那十五個固定的鍵,而理由是清單上會變的散文,抄進去就是抄一份改不掉的舊值);清單只有十一欄、也就是還沒有 `probe` 那一欄時,全部照 `way` 推 `action`、行為與加上那一欄之前一模一樣,只印一行 note 講明整份清單沒有那一欄,不逐列印警告;七個結束碼各有處置:0 對齊完成(零筆改動也算)、1 清單讀不到、2 清單讀到了卻解不出任何可交項目、3 找不到 `tasks.sh`,這三碼一律「一筆都沒動」且不得回報成清單上沒有可交項目,4 是部分失敗、逐筆帶 `tasks.sh` 的結束碼、成功的那幾筆算數,5 檔案系統失敗,6 呼叫寫錯(含 `--root` 不是絕對路徑、`--allow-cond` 形狀不對)。這一支只呼叫 `tasks.sh` 的 `list`、`add`、`remove` 三個子命令,一次都不自己動 `tasks/` 底下的檔案,也一次都不帶 `--force`。事件偵測與到期判定是本 domain 的 `tools/due.sh`,三個子命令 `scan`、`events`、`next`:`scan` 一輪一次,比對狀態快照算出本輪新事件、推進快照與事件計數,再逐筆判到期;`events` 是唯讀預覽,一律不推進快照;`next` 是純算,給一組欄位算出 `next_run`。七個結束碼各有處置:0 判完了、1 有狀態來源存在卻讀不到(結果照樣印得出來,那個來源本輪不發事件)、2 有待辦的欄位值判不了(其餘各筆照判)、3 快照換不上去(同一批事件下一輪會被判第二次,要吵出來)、4 待辦簿目錄不存在或零筆(不是失敗,但「沒判過」不等於「都沒到期」)、5 檔案系統失敗、6 呼叫寫錯。事件靠比對狀態快照,一個產生者的腳本都不改:工作包鎖檔轉態、`sessions/{sid}.stage` 換值、`errors/hooks.jsonl` 新增列、`sessions/{sid}.start` 與 `.end`、`worklog-pending` 暫存區清空,各對一個事件名;`analyze-completed:{HASH}` 的來源在 wiki 的分析頁上,要連網才判得出來,這一輪標成未接線並吵出來,不靜靜當成還沒發生,`cron:{式子}` 同樣未接線。快照比對有一個明確的代價:**兩輪之間發生又消失的事件會漏掉**,假設「事件不會漏」就會出錯,而那種錯是無聲的。`tasks/` 底下的檔案只有 `start` 那一步的 `seed-tasks.sh apply` 會經 `tasks.sh` 動到,`patrol`、`status`、`stop` 三個操作一律只讀:`patrol` 一次都不跑 `seed-tasks.sh`(無人值守那一輪移除一筆會把那一筆的 `last_run` 與 `fail_count` 一起弄丟,而清單同步到一半就會刪錯,破壞性清理留給人),`status` 只跑 `plan`、那個子命令一律不寫。代價要講明:沒有人 `start` 也沒有人看的機器上,清單改動要等下一次 `start` 才進得了待辦簿。到期的內建項由本 domain 的 `tools/run-due.sh` 真的執行,兩個子命令 `run` 與 `plan`(`plan` 等同 `run --dry-run`,只印不跑也不回寫),由 `patrol` 那一輪在寫心跳之前叫一次;它只跑動作是一行指令、而且 `spec_key` 非空的那幾筆,動作是技能名的、只提醒的、還有使用者親手登錄的一律不跑(叫用整支技能會寫頁、開 PR、改檔案,代價與唯讀盤點差太多,要另外判一次);`{cli}` 由 `jsc-cli/tools/detect-clis.sh` 代入、一個目標跑一次、全部成功那一筆才算成功,`{repo}` 的存取庫掃描還沒做出來所以帶那個代入點的一律只印 `held=` 跳過;執行前再驗一次指令形狀(不得有金錢符號、反引號、波浪號、分號、管線與連接符號,路徑一定要落在這一輪的根目錄底下,代完之後不得留大括號),驗不過同樣只印 `held=`;**`held=` 那幾筆的 `last_run` 與 `fail_count` 一個字都不動**——代不出目標不是那一筆做錯了什麼,記成失敗會讓一個沒有人修得動的計數蓋掉真的壞掉的那幾筆;成功回頭叫 `tasks.sh done`、下一次到期由 `due.sh next` 算,失敗回頭叫 `tasks.sh fail`,失敗照重試、不自動退讓;六個結束碼各有處置:0 這一輪處理完(零筆到期與全部 `held=` 都算)、1 有指令回非零(那一筆已記成失敗,是發現不是故障)、2 到期清單讀不到或欄位對不上因而一筆都沒跑(不得讀成「都沒到期」)、4 有回寫失敗(跑了沒記到,下一輪會再跑一次同一筆)、5 檔案系統失敗、6 呼叫寫錯,**任何一碼都不中止那一輪**。`tasks.sh` 的 `pause` 與 `resume` 兩個子命令還沒有任何一個操作呼叫得到:登錄時的補問流程、逾期與失敗的處理行為、`remind` 怎麼送到前景、待辦簿的 wiki 雙向同步,四項都還沒接上去,所以到期的那幾筆這一輪只印出來、不執行,也不回寫 `last_run`;執行那一步接上來的時候,代入點的展開歸它負責——`action` 帶大括號的那幾筆要先把 `{cli}` 換成每一支偵測到的 CLI 代號、`{repo}` 換成每一個掃到的存取庫工作目錄,一個目標跑一次,代不出目標就當這一筆這一輪沒得跑並回報,一律不得把帶大括號的字面值原樣送進殼。呼叫端沒講清楚要哪一個操作時走 `jsc-ask:ask` 的決策樹問,但 `patrol` 那一路一律不問。不參與閘門判定 | +| 外部呼叫 | 工具一律走前置步驟取得的根目錄底下那一組不帶版本的路徑(本頁記成 `{CURRENT}`,實際填的是像 `/root/.jsc/current` 這種字面絕對路徑):`{CURRENT}/jsc-assist/tools/patrol.sh`、`{CURRENT}/jsc-assist/tools/schedule.sh`、`{CURRENT}/jsc-assist/tools/due.sh`、`{CURRENT}/jsc-hooks/hooks/heartbeat.sh`,wiki 那一支是 `{CURRENT}/jsc-gitea/tools/gitea.sh`,目錄頁那一支是 `{CURRENT}/jsc-gitea/tools/wiki-contents.sh`,連結驗證那一支是 `{CURRENT}/jsc-gitea/tools/link-check.sh`,執行狀態事件那一支是 `{CURRENT}/jsc-hooks/tools/report-status.sh`,範本是 `{CURRENT}/jsc-assist/templates/monitor-contents.md`;`JSC_HOME` 沒設時,人在現場那一次 `readlink` 自己退回 `~/.jsc` 再解,排程那一輪則直接用條目餵進來的值,指令列上不留變數也不留波浪號;不拿技能提示給的快取基底目錄組工具路徑——權限只放行 current 那一組,快取路徑帶版本號,規則寫成萬用字元也對不上,用錯路徑會被靜靜擋掉。`jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。`write` 只由 `tools/patrol.sh finish` 呼叫,技能自己不呼叫。本 domain 的 `tools/schedule.sh` 的 `install`、`remove`、`status` 三個子命令:`install` 會查 `{CURRENT}/jsc-assist` 與 `{CURRENT}/jsc-gitea` 兩個連結在不在、不在就警告且不代建,會把巡檢的 CLI 用 `command -v` 解成絕對路徑、把 `PATH`、`GITEA_HOST`、`GITEA_TOKEN`、`JSC_HOME`、`JSC_ASSISTANT_HEARTBEAT_TTL` 與所有已設定的 `JSC_WIKI_REPO` 系列快照進條目(`PATH` 一定要帶:cron 給的只有 `/usr/bin` 與 `/bin`,而 CLI 執行檔多半裝在家目錄底下,少了它那一輪偵測 CLI 會回結束碼 0 加一份空清單,看起來像這台機器沒裝 CLI)(含內容頁的 `JSC_WIKI_REPO_MONITOR` 與目錄頁的 `JSC_WIKI_REPO_CONTENTS`,名單當下從環境撈、不寫死,新頁型自動涵蓋)、條目自帶 `JSC_GITEA_CONFIRM=yes`、刻意不寫死工作階段代號(寫死會讓那一輪的 `start` 與 `end` 落在不同的代號上:前者由 hook 從標準輸入的 JSON 讀 CLI 真正的代號,後者由工具腳本只讀得到環境變數,兩半永遠配不起來,每一輪都被算成一支中止的技能)、把自己解好的字面根目錄寫進條目的提示文字(固定格式 `工具根目錄={字面絕對路徑}`,那一輪就是從這裡讀根目錄)並印成 `patrol_root=`、`--patrol-cmd` 或 `JSC_ASSIST_PATROL_CMD` 給的自訂指令沒帶那一段時只警告不中止、並印出這一輪要開的 `allow_rule=` 規則(七支腳本各三種呼叫形式,含 `gitea.sh`、`wiki-contents.sh`、`link-check.sh` 與 `jsc-hooks/tools/report-status.sh`——`Skill(jsc-gitea:wiki)` 只放行叫用技能,技能內部的 Bash 呼叫仍各自受檢;路徑是 `current` 那一組確切路徑,不用萬用字元);七個結束碼各有處置:0 往下走、1 是條目裝了但 cron 沒在跑要照實講不會執行、2 是缺 jsc-hooks 導致門檻讀不到、3 是這台機器沒有排程機制、4 是排程操作失敗要原樣引用 stderr、5 是回讀驗證失敗要叫人自己去看 `crontab -l`、6 是呼叫寫錯,含 `install heartbeat`、週期塞不進門檻、判不出 CLI、那一支 CLI 的執行檔不在 `PATH` 上,以及 `JSC_HOME` 解不出絕對路徑(條目寫不出字面根目錄)。本 domain 的 `tools/patrol.sh` 的 `collect`、`finish`、`abort` 三個子命令,七個結束碼各有處置:0 往下走、1 部分失敗照樣寫頁、2 是 finish 找不到 heartbeat.sh 要回報「記下來了但沒有心跳」、3 是各項全失敗照樣寫頁且判定異常、4 是讓開或鎖被搶走一律不寫心跳、5 是檔案系統失敗要吵出來、6 是呼叫寫錯。巡檢那五項讀 `jsc-log/tools/usage-stats.sh`、`jsc-hooks/hooks/version-guard.sh report`、`jsc-hooks/hooks/restart-gate.sh report`、`$JSC_HOME/sessions/*.stage`、`$JSC_HOME/wp/*.pr`、`heartbeat.sh report`、`jsc-hooks/tools/report-status.sh drain` 與 `rotate`,除了排空會把事件流的位移往前推之外全部只讀,任一項失敗不影響其餘各項。`report-status.sh` 三個結束碼各有處置:0 是排空到新事件、3 是沒有新事件(正常狀態,不是失敗)、2 是呼叫寫錯;找不到這一支、`drain` 回 0 與 3 以外的碼、或 `rotate` 回非 0,都只讓這一項標成失敗或記一筆警示,一律不中止那一輪——回報鏈自己壞掉不可以把被回報的那一輪拖下去。`rotate` 只在 `drain` 成功時緊接著跑:中間隔越久,那段時間新寫進來的事件被搬進備份檔而從此排不到的機會越大;排空失敗時位移狀態未知,這時候輪替會直接吃掉還沒排空的那一批。配對以 `session` 加 `name` 為鍵,不只看 `name`:五支 CLI 併發時同一支技能會有好幾個工作階段同時在跑。沒配對到的 `start` 留在 `$JSC_HOME/assistant/events-open.tsv` 跨輪繼續配對,開超過心跳門檻才算疑似中止,未達門檻的算還在跑,超過一天沒配對到就丟掉。wiki 讀寫一律經 `jsc-gitea:wiki`,技能自己不拼 API 呼叫;只有目錄頁那一個 H2 區塊例外,走 `jsc-gitea/tools/wiki-contents.sh upsert`,它自己解 `CONTENTS` 存取庫、自己讀回整頁比對標題,舊頁還是 markdown 表格時自己先整頁轉成 H2 區塊再寫,七個結束碼各有處置:0 已更新或已新增、1 組不出頁面內容或寫入失敗要 `abort`(找不到同名標題不算錯,那是附加)、2 參數錯就改正重跑(範本路徑不存在也回這一碼,代表 plugin 沒裝齊)、3 是 `CONTENTS` 存取庫未設定且**不中止這一輪**、4 是頁不存在又沒給範本,本技能一律帶第五個參數所以不會出現、7 金鑰失效要 `abort`、8 其他 API 失敗要 `abort`。比對鍵取 H2 標題,也就是內容頁頁名 `MONITOR_{HASH}`,不取「監控頁」那一條的連結:連結含 `GITEA_HOST` 與頁名的網址編碼,那三樣一變鍵就對不上,同一台機器每輪多附一個區塊;頁名只由 `{主機名}/{登入帳號}` 決定,那三樣都動不到它。連結一律寫成 `[{文字}]({絕對網址})`,網址只取 `gitea.sh wiki-url` 印的那一個、不自己組路徑,那一支的結束碼 4、5、7、8 與其餘非 0 各有處置;每一個要放進頁面的連結在寫入前先過 `jsc-gitea/tools/link-check.sh`,它每個網址印一行 `{OK|DEAD|SKIP}` 加網址加說明,五個結束碼各有處置:0 才准寫入、1 有連不到的就不寫並回報 DEAD 那幾筆、2 是一個網址都沒給要補參數重跑、3 是 `GITEA_HOST` 未設定要先設定且不得跳過驗證、7 是金鑰失效要停下來回報金鑰問題而不是當成死連結;驗證走 API 不看網頁狀態碼,私有存取庫的網頁網址對未登入請求一律回 404。頁名雜湊一律取 `gitea.sh hash-id`/`tools/hash-id` 印的完整 40 碼大寫十六進位,不截短、不加前綴、不手算,空輸入回 2。crontab 與 schtasks 一律經 `tools/schedule.sh`。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。待辦簿的存放格式與讀寫入口是本 domain 的 `tools/tasks.sh`:一筆一檔、純文字 key=value、十五個鍵順序固定、值是空的照樣寫出那一行,讀的時候只在第一個等號斷開,寫的時候把值折成一行,一筆一檔的理由同 `restart-required.d`(並行寫入不互相覆寫),`id` 取共用 hash 規則那四十碼的前 8 碼、碰撞時每次加長兩碼,七個子命令 `list`、`add`、`done`、`fail`、`pause`、`resume`、`remove` 與八個結束碼的完整說明寫在那一支的檔頭;第十五個鍵是 `spec_key`,值是 `jsc-{domain}:{技能名}`,那是從一支技能反查到它對應那一筆內建項的唯一把手(`id` 是建立時間加標題的雜湊、反查不了;標題與 `action` 拿來當鍵會撞上使用者交辦的那幾筆),只有 `origin=assistant` 帶得上它,`--spec-key` 配 `--origin user` 回 2,`remove` 對 `origin=user` 的那一筆一律回 7、除非人親自帶 `--force`。內建定期檢查項照委派清單種入與重建的入口是本 domain 的 `tools/seed-tasks.sh`,兩個子命令 `plan`(唯讀預覽)與 `apply`(真的做),清單路徑取 `--root` 餵進來的那一個字面絕對路徑底下的 `jsc-meta/tools/delegate-spec.tsv`;欄位對映是 `trigger` 與 `recur` 原樣抄、`spec_key` 由清單前兩欄合成、`action` 由 `way` 與第十二欄 `probe` 一起推、`title` 固定寫成「委派清單內建項:{spec_key}」以免清單一改就換 `id`、`kind` 一律 `check`、`origin` 一律 `assistant`、`repo` 與 `due` 一律留空,清單的 `verdict`、`slice`、`human`、`next`、`version` 與它自己的 `origin` 欄一律不抄(清單的 `origin` 是 `seed` 或 `judged`,與待辦簿的 `origin` 同名不同義);`action` 那一欄的推法分兩路:`way` 含 `invoke` 就取技能名、`probe` 連看都不看(填了指令會讓整支交出變成只跑一支腳本,那支技能該寫的頁一頁都不會寫,所以那是清單填錯,照 `way` 取技能名並印一行 `probe_bad=`),其餘那幾種交出方式照 `probe` 走——一行指令就取那一行指令、`pending:{理由}` 取 `remind` 並印一行 `pending=`、減號或空的取 `remind`;`probe` 代不進去一律退回 `remind` 並印一行 `probe_bad=`,照樣種入那一筆,涵蓋路徑不是 `{root}/jsc-{domain}/` 開頭、指令裡有金錢符號或波浪號、出現三個代入點以外的大括號、代不出根目錄、代出來的腳本不在這台機器上五種(不種入等於讓上游一格填錯把一筆帶著 `last_run` 與 `fail_count` 的內建項刪掉);`{root}` 由這一支代成 `--root` 給的字面絕對根目錄(沒給就從清單位置往上推三層),那一層的目錄名以 `jsc-{domain}` 為準、找不到才退回不帶前綴的 `{domain}`,而 `{cli}` 與 `{repo}` 刻意留在值裡不展開——那兩件事種入的當下還不知道,種入時展開成多筆會讓同一個 `spec_key` 有好幾個檔案、一致化每一輪只印 `dup=`,而且 CLI 或存取庫一變就要移除再重新登錄、歷史跟著歸零;**`action` 裡出現大括號就是還沒代好的代入點,任何讀取端一律不得原樣拿去執行**,展開由往後接上來的執行那一步負責,`{cli}` 換成每一支偵測到的 CLI 代號、`{repo}` 換成每一個掃到的存取庫工作目錄,一個目標跑一次,所有目標的結果合起來算這一筆的一次成敗;`pending` 的那幾筆只印在回報裡、待辦檔上一個字都不加(寫進標題會換 `id` 又比不出漂移,另立欄位要動待辦簿那十五個固定的鍵,而理由是清單上會變的散文,抄進去就是抄一份改不掉的舊值);清單只有十一欄、也就是還沒有 `probe` 那一欄時,全部照 `way` 推 `action`、行為與加上那一欄之前一模一樣,只印一行 note 講明整份清單沒有那一欄,不逐列印警告;七個結束碼各有處置:0 對齊完成(零筆改動也算)、1 清單讀不到、2 清單讀到了卻解不出任何可交項目、3 找不到 `tasks.sh`,這三碼一律「一筆都沒動」且不得回報成清單上沒有可交項目,4 是部分失敗、逐筆帶 `tasks.sh` 的結束碼、成功的那幾筆算數,5 檔案系統失敗,6 呼叫寫錯(含 `--root` 不是絕對路徑、`--allow-cond` 形狀不對)。這一支只呼叫 `tasks.sh` 的 `list`、`add`、`remove` 三個子命令,一次都不自己動 `tasks/` 底下的檔案,也一次都不帶 `--force`。事件偵測與到期判定是本 domain 的 `tools/due.sh`,三個子命令 `scan`、`events`、`next`:`scan` 一輪一次,比對狀態快照算出本輪新事件、推進快照與事件計數,再逐筆判到期;`events` 是唯讀預覽,一律不推進快照;`next` 是純算,給一組欄位算出 `next_run`。七個結束碼各有處置:0 判完了、1 有狀態來源存在卻讀不到(結果照樣印得出來,那個來源本輪不發事件)、2 有待辦的欄位值判不了(其餘各筆照判)、3 快照換不上去(同一批事件下一輪會被判第二次,要吵出來)、4 待辦簿目錄不存在或零筆(不是失敗,但「沒判過」不等於「都沒到期」)、5 檔案系統失敗、6 呼叫寫錯。事件靠比對狀態快照,一個產生者的腳本都不改:工作包鎖檔轉態、`sessions/{sid}.stage` 換值、`errors/hooks.jsonl` 新增列、`sessions/{sid}.start` 與 `.end`、`worklog-pending` 暫存區清空,各對一個事件名;`analyze-completed:{HASH}` 的來源在 wiki 的分析頁上,要連網才判得出來,這一輪標成未接線並吵出來,不靜靜當成還沒發生,`cron:{式子}` 同樣未接線。快照比對有一個明確的代價:**兩輪之間發生又消失的事件會漏掉**,假設「事件不會漏」就會出錯,而那種錯是無聲的。`tasks/` 底下的檔案只有 `start` 那一步的 `seed-tasks.sh apply` 會經 `tasks.sh` 動到,`patrol`、`status`、`stop` 三個操作一律只讀:`patrol` 一次都不跑 `seed-tasks.sh`(無人值守那一輪移除一筆會把那一筆的 `last_run` 與 `fail_count` 一起弄丟,而清單同步到一半就會刪錯,破壞性清理留給人),`status` 只跑 `plan`、那個子命令一律不寫。代價要講明:沒有人 `start` 也沒有人看的機器上,清單改動要等下一次 `start` 才進得了待辦簿。到期的內建項由本 domain 的 `tools/run-due.sh` 真的執行,兩個子命令 `run` 與 `plan`(`plan` 等同 `run --dry-run`,只印不跑也不回寫),由 `patrol` 那一輪在寫心跳之前叫一次;它只跑動作是一行指令、而且 `spec_key` 非空的那幾筆,動作是技能名的、只提醒的、還有使用者親手登錄的一律不跑(叫用整支技能會寫頁、開 PR、改檔案,代價與唯讀盤點差太多,要另外判一次);`{cli}` 由 `jsc-cli/tools/detect-clis.sh` 代入、一個目標跑一次、全部成功那一筆才算成功,`{repo}` 的存取庫掃描還沒做出來所以帶那個代入點的一律只印 `held=` 跳過;執行前再驗一次指令形狀(不得有金錢符號、反引號、波浪號、分號、管線與連接符號,路徑一定要落在這一輪的根目錄底下,代完之後不得留大括號),驗不過同樣只印 `held=`;**`held=` 那幾筆的 `last_run` 與 `fail_count` 一個字都不動**——代不出目標不是那一筆做錯了什麼,記成失敗會讓一個沒有人修得動的計數蓋掉真的壞掉的那幾筆;成功回頭叫 `tasks.sh done`、下一次到期由 `due.sh next` 算,失敗回頭叫 `tasks.sh fail`,失敗照重試、不自動退讓;六個結束碼各有處置:0 這一輪處理完(零筆到期與全部 `held=` 都算)、1 有指令回非零(那一筆已記成失敗,是發現不是故障)、2 到期清單讀不到或欄位對不上因而一筆都沒跑(不得讀成「都沒到期」)、4 有回寫失敗(跑了沒記到,下一輪會再跑一次同一筆)、5 檔案系統失敗、6 呼叫寫錯,**任何一碼都不中止那一輪**。`tasks.sh` 的 `pause` 與 `resume` 兩個子命令還沒有任何一個操作呼叫得到:登錄時的補問流程、逾期與失敗的處理行為、`remind` 怎麼送到前景、待辦簿的 wiki 雙向同步,四項都還沒接上去,所以到期的那幾筆這一輪只印出來、不執行,也不回寫 `last_run`;執行那一步接上來的時候,代入點的展開歸它負責——`action` 帶大括號的那幾筆要先把 `{cli}` 換成每一支偵測到的 CLI 代號、`{repo}` 換成每一個掃到的存取庫工作目錄,一個目標跑一次,代不出目標就當這一筆這一輪沒得跑並回報,一律不得把帶大括號的字面值原樣送進殼。呼叫端沒講清楚要哪一個操作時走 `jsc-ask:ask` 的決策樹問,但 `patrol` 那一路一律不問。不參與閘門判定 | | 完成條件 | 四個操作都要先取得工具根目錄,之後每一支腳本都拿那一個字面絕對路徑呼叫;排程那一輪只從叫用文字取,取不到就回報條目沒帶根目錄並中止,收尾狀態取 `aborted`,不得改跑 `readlink` 或任何解析指令,也不得改用帶變數的路徑硬跑;人在現場叫用時取不到才自己解一次,解出來的要是一條存在的絕對路徑(同一步用 `[ -d ]` 查過),解不出來或目錄不存在就回報缺 `current` 並中止,同樣取 `aborted`。`start` 要先跑過一次 `seed-tasks.sh apply` 並把它的結束碼、`added=`、`removed=`、`kept=`、`drift=`、`held=`、`bad=`、`probe=`、`pending=`、`probe_bad=` 各數字與逐列 `held=`、`bad=`、`drift=`、`dup=`、`skip_user=`、`probe=`、`pending=`、`probe_bad=` 記進收尾回報(`pending=` 那幾筆要寫成「有唯讀盤點入口、還沒接上」並附清單上的理由原文,不得跟本來就只提醒的那幾筆混成一句;`probe=` 那幾筆的 `holes=` 不是減號時要寫明還留著哪幾個代入點)(回 1、2、3 時要寫成「內建項原樣沒動」並附原因,不得寫成「沒有可交項目」),然後要那一輪巡檢的 `finish` 回 0 且 `report` 回 `state=fresh`,才算啟動成功;巡檢沒寫成心跳一律回報失敗並停下,不得宣稱啟動;`schedule.sh install patrol` 回 1 要講明條目不會被執行與 `sudo service cron start`,不得宣稱排程會定時執行;回 0 或 1 都要把 `allow_rule=` 各行、「條目含金鑰快照、變數改了要重裝」這句提醒,以及 `current` 連結缺漏的警告轉出去。`patrol` 要五項各自有 `status`、「待辦簿到期與逾期」那一節要有逐筆判定表(`due.sh` 回 0、1、2、3、4 都算判過,其中 1、2、3 各記一筆警示與一列待人處理;回別的碼就照實寫「這一輪判不出到期」並說明那不代表沒有任何一筆到期,不留空白也不寫成「都沒到期」)、執行狀態事件那一項要印出本輪事件數、非 ok 事件數與未配對的 `start`(`drain` 回 3 是沒有新事件,照樣算這一項讀到底)、監控頁那一頁要放的連結全部通過 `link-check.sh`(或整頁本來就沒有連結)、監控頁三塊重組寫成、目錄頁那一個 H2 區塊的網址通過 `link-check.sh` 後更新成功,或以目錄頁結束碼 3、或以連結驗證非 0 回報成沒更新、`finish` 回 0,才算一輪跑完;`collect` 回 4 是讓開,不算失敗也不寫任何東西;舊頁讀不回來就不寫,回報「這一輪沒有結果」;連結驗證沒過就不寫那一頁,監控頁沒寫成就 `abort`,心跳一定不寫;目錄頁除了結束碼 3 之外的非 0 也一樣 `abort`,結束碼 3 只少一筆索引,那一輪的結果已經在監控頁上,照樣寫心跳並把缺的變數列進待人處理;目錄頁那一個區塊的連結驗不過同樣只少一筆索引,照樣寫心跳並把那一筆列進待人處理。`status` 要印出現況表,或印出「助理未運行」並說明原因,並且要多印一段內建項與委派清單的差異(該加幾筆、還剩幾筆孤兒、保留的 `cond` 各是哪一支、`drift=` 各要換什麼值,以及「要套用就跑 `start`」這句話),那一段比不出來就照實說比不出來;心跳不存在、待辦簿目錄不存在、待辦簿零筆、排程沒裝、清單讀不到,五種都算正常結束。`stop` 要 `schedule.sh remove all` 先回 0、`clear` 再回 0,並印出帶三段話的停止訊息;`remove` 非 0 就回報排程還在、助理停不掉,不清心跳也不印停止訊息;`clear` 回 5 就回報心跳檔還在、助理沒有確實停掉,不印停止訊息。四個操作都要在回報之後寫一筆 `skill-end`,`status` 取 ok、blocked、failed、degraded、aborted 五選一,要與回報出去的結果一致;那一支回非 0 只回報成回報鏈的缺陷,不改寫這一次操作的成敗 | | 可驗證跡象 | 四個操作的轉錄裡,每一條指令列都是字面絕對路徑,開頭是 `/`,沒有 `$JSC_HOME`、`${JSC_HOME}` 或 `~`,也沒有任何一次因為路徑帶變數而跳出來的權限詢問;排程那一輪從頭到尾一次 `readlink`、一次 `ls` 都沒有,根目錄直接取自叫用文字;人在現場那一路才可能有 `readlink`,而且同一次叫用只出現一次。`start` 之後 `$JSC_HOME/assistant/heartbeat` 存在,`ts` 是剛才那一輪的時間,`crontab -l` 找得到一筆帶 `# jsc-assist:assistant patrol` 的條目,而且只有一筆,帶 `# jsc-assist:assistant heartbeat` 的舊條目一筆都不剩;那一筆條目裡的 CLI 是絕對路徑,前面帶著 `JSC_GITEA_CONFIRM=yes` 與環境變數快照,提示文字裡有「工具根目錄=」接一個字面絕對路徑,那個值與 install 印的 `patrol_root=` 和 `allow_rule=` 用的根目錄完全相同,不是變數也不是快取實體路徑;install 印出的 `allow_rule=` 都是 current 那一組展開後的字面絕對路徑,沒有變數、沒有波浪號、沒有萬用字元,也沒有 `Write(...)`,而且 `jsc-gitea/tools/link-check.sh` 與 `jsc-hooks/tools/report-status.sh` 那三種呼叫形式都在裡面。`patrol` 跑完之後 wiki 的 `MONITOR_{HASH}` 只有三塊:基本資料一字未改、最新一輪換成本輪、摘要表最上面一列是本輪且總列數不超過 24,頁名的 `{HASH}` 是 40 碼大寫十六進位,雜湊來源那一列寫的是不含網域的短主機名;`CONTENTS` 存取庫裡的 `MONITOR_CONTENTS` 只有自己那一個 H2 區塊變動,同一台機器從頭到尾只有一個區塊,標題是 `MONITOR_` 接 40 碼大寫十六進位、標題上不帶連結也不帶網址,區塊裡「監控頁」那一條是 `[{頁名}]({絕對網址})` 這種連結、點下去開得起那一頁,「HASH」那一條是裸 HASH、40 碼大寫十六進位、不帶連結,八條欄位一條都不缺、格式是 `- {欄位名}:{值}`,頁上一個 markdown 表格都不剩,兩頁上點得到的連結沒有一個是死的——把頁上的網址抓出來重跑一次 `link-check.sh`,應該全部是 `OK`、結束碼 0,別台機器的區塊一字不動,`$JSC_HOME/assistant/patrol/` 底下有本輪的 `latest.md`、`summary.md`、`summary-row.md`、`newpage.md`、`contents-entry.md`,摘要列是五欄、警示來源那一欄有值或寫「無」;兩支腳本不是從 current 那一組路徑跑起來時,stderr 會有一行 `[WARN]` 點出實際路徑與應該用的路徑,`$JSC_HOME/assistant/usage-prev.tsv` 換成本輪的累計數,`$JSC_HOME/assistant/events-prev.tsv` 換成本輪的狀態快照(三欄定位字元分隔,每一個來源另有一列 `meta`)、`$JSC_HOME/assistant/events-seen.tsv` 是每一個事件名的累計次數與最後發生時間,`$JSC_HOME/assistant/patrol/due/` 底下有本輪的 `due.md` 與 `rows.txt`(`rows.txt` 是十一欄定位字元分隔,欄位順序印在 `rows_columns=`;要逐筆取值就讀它,不要切 `task=` 那幾行,那幾行的四個欄位都可能帶空白),第一次跑那一輪的 `due.sh` 印 `first_run=1`、事件數為 0,且 `tasks/` 底下一個檔案都沒被改動,`$JSC_HOME/assistant/patrol.lock` 已經放掉;監控頁的最新一輪有「執行狀態事件」那一節,節裡有本輪事件數、非 ok 事件數,以及非 ok 明細與未配對 `start` 兩張表(一筆都沒有時寫明「沒有」,不留空表格);`$JSC_HOME/usage/scan-state/events.offset` 的數字往前推到本輪排空的位置,`$JSC_HOME/assistant/events-open.tsv` 只剩下還沒配對到 `end` 的那幾筆。讓開的那一輪沒有任何寫入跡象。`stop` 之後心跳路徑不存在,`crontab -l` 找不到任何 `# jsc-assist:assistant` 條目。以上都不動別人的排程條目,條目數量前後相同。`status` 無寫入跡象,只有回報內容。四個操作跑完,`$JSC_HOME/usage/events.jsonl` 最後都多一筆 `name` 是 `jsc-assist:assistant`、`phase` 是 `end` 的事件,`status` 與回報出去的結果一致,而且同一個 `session` 下它與 hook 記的那一筆 `phase=start` 配得起來。`patrol`、`status`、`stop` 三個操作都不動 `tasks/` 底下的檔案;`start` 只在第一步經 `tasks.sh` 動內建項,動完之後 `tasks/` 底下每一個 `origin=user` 的檔案內容與修改時間都一字未改,`origin=assistant` 且帶 `spec_key` 的那幾筆與委派清單上非 `none`、非 `cond` 的那幾列一對一對得上(同一個 `spec_key` 只有一個檔案),`spec_key` 是空的那幾筆一筆都沒被加也沒被刪,判定是 `cond` 的那幾支在 `tasks/` 底下找不到對應檔案而回報裡逐支有一行 `held=`;`way` 只有 `patrol` 或 `remind`、而 `probe` 是一行指令的那幾筆,`action=` 那一行是完整字面絕對指令,開頭是 `/` 或一個大寫環境變數指派,路徑中段沒有版本號、沒有金錢符號、沒有波浪號,代入點只可能剩 `{cli}` 或 `{repo}`,而且那幾行在回報裡各有一行 `probe=` 對得上;`probe` 是 `pending:` 的那幾筆 `action=remind`、待辦檔上看不出跟本來就只提醒的那幾筆有什麼不同,差別只在回報裡的 `pending=` 那幾行;清單只有十一欄時 `probe=`、`pending=`、`probe_bad=` 三個數字全是 0,而且 stderr 有一行 note 講明那一份清單沒有第十二欄。四個操作都不動 worktree 與程式碼存取庫。排程的 log 一律在 `$JSC_HOME/assistant/schedule.log`,不落在任何存取庫 | diff --git a/tools/schedule.sh b/tools/schedule.sh index 02fb50b..453d16d 100755 --- a/tools/schedule.sh +++ b/tools/schedule.sh @@ -336,7 +336,15 @@ patrol_command() { # 不寫死是刻意的:頁型會增加,目錄頁後來也分了自己的存取庫變數,寫死清單就要跟著改一次, # 而漏掉的那一個在排程那一輪是空值,wiki 寫不成、心跳不寫,外面只看得到心跳過期。 snapshot_names() { - printf '%s\n' GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL + # PATH 也要快照。cron 給的 PATH 只有 /usr/bin 與 /bin,而這台機器的 CLI 執行檔多半裝在 + # 家目錄底下,所以那一輪偵測 CLI 會一支都找不到——而且是**回結束碼 0、印出空清單**, + # 不是失敗。看起來像「這台機器沒裝 CLI」,實際上是 PATH 上找不到。 + # 2026-09-04 實測踩到:無人值守那一輪 clis=0,帶 {cli} 代入點的內建項全部代不出目標, + # 而同一支腳本在有人的 shell 裡回五支。 + # 巡檢那一輪要跑的指令本身走的是字面絕對路徑、不靠 PATH;靠 PATH 的是那幾支腳本自己 + # 呼叫的外部程式,所以修在條目這一層,不是逐支腳本各自去猜安裝路徑——猜就要維護一份 + # 路徑清單,而清單會過期。 + printf '%s\n' PATH GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL env 2>/dev/null \ | sed -n 's/^\(JSC_WIKI_REPO\)=.*/\1/p; s/^\(JSC_WIKI_REPO_[A-Za-z0-9_]*\)=.*/\1/p' \ | sort -u -- 2.53.0