助理要記住兩種項目——內建的定期檢查項與使用者當面交辦的事——但目前沒有地方放。待辦簿是助理待辦與建議下一個指令兩批功能的共同地基,先把存放格式與讀寫工具做出來,判定邏輯之後才接。 存成一筆一檔,純文字 key=value。一筆一檔的理由同重啟閘門那組檔案:並行寫入不互相覆寫。五支 CLI 加上排程那一輪有可能同時動待辦簿,整本存成一個檔案的話兩邊各讀一次整檔、各改自己那一筆、各寫回整檔,後寫的那一次就把前一次整本蓋掉,而且沒有任何訊號。同一個 id 被同時寫也不會寫出半份:先寫暫存檔再搬過去,搬在同一個檔案系統上是原子操作。暫存檔名以點號開頭,列表跳過點號開頭的檔案,寫到一半的那一份不會被列出來。 規格的欄位表少一個欄位。id 是建立時間加標題的雜湊,但表上沒有存建立時間的欄位,不存它就再也算不回同一個 id、驗不出檔名對不對、前綴碰撞時也接不下去。補上建立時間,所以一筆是十四個鍵。 id 取完整雜湊的前八碼,不跟 wiki 頁名的四十碼一致。兩者用途不同:四十碼那條規則管的是 wiki 頁名,頁名一撞就是兩台機器的紀錄互相覆寫且看不出來,所以不准截短;這裡的 id 是本機檔名,人要念它、打進指令、看它印在狀態表與提醒文字裡,四十碼沒有人讀得完也打不對,人就會改用「第三筆」這種說法指定要關哪一筆,那才是真正會關錯的地方。前八碼是同一個雜湊的前綴不是另一套算法,拿建立時間與標題重算就驗得回來。前綴撞上而內容不同就每次多取兩碼再試,不在後面補序號——補了就算不回來。建立時間與標題都相同的不是碰撞而是同一筆被登錄兩次,一律不寫並回專屬結束碼,因為定期檢查項會因為清單重建而重跑登錄,靜靜多寫一筆會讓同一個檢查每輪做兩次。 操作補到六個,兩個都是補規格的洞。失敗那一個是因為規格說失敗要累加連續失敗次數,但原本四個操作沒有一個寫得到它,少了它那個計數永遠是零,監控頁與提醒上那句「已連續失敗幾次」永遠是零次,於是一個壞掉的項目每輪重試而沒人知道,那正是這個欄位要防的事。恢復那一個是因為停用只由人設,也就只有人解得開,沒有別的元件寫得出這個轉移;只給停用不給恢復就是一道單向門,人只能去手改檔案,而手改繞過了轉移表。 上次執行與下次執行不另開操作:完成與失敗都吃選項把值餵進來,值由算到期那一邊算好。這一支不算下一次是什麼時候,兩邊各算一次就會漂移。也刻意沒有編輯操作:id 由建立時間與標題算出來,改掉標題之後 id 就對不回去了。 狀態轉移寫成明確的表並在腳本裡擋住不合法的轉移。停用之後不收完成,因為停掉的那一筆助理本來就沒在跑,標成做完等於偷偷解開又收掉;停用之後不收失敗,因為助理沒在跑它就不可能是它失敗;收掉的那一筆不再有下一次,四種操作全擋。恢復刻意不把連續失敗次數歸零,那幾次失敗真的發生過,歸零會把提醒抹掉而那筆一恢復就會照樣再失敗,要歸零就等它跑成功一次。 值裡有等號或換行用兩條約定處理,不發明跳脫規則。讀的時候只在第一個等號斷開,十四個鍵都是固定的詞、一個都不含等號。寫的時候把值折成一行並在標準錯誤記一行說折過了。選折行不選跳脫,是因為讀的人不只這一支腳本,技能本文與巡檢那一輪都直接把檔案當 key=value 讀,跳脫規則要每個讀的人各自實作一次,漏掉一個就會把跳脫序列原樣印進報告或監控頁,而那看起來很像正常內容。 根目錄解析退回使用者家目錄底下那一層,跟排程、巡檢與心跳三支退回同一個值——兩邊退回的位置不同,待辦簿就會躲在一個沒有人去讀的目錄裡,而每一支都自認為讀對了。連家目錄也沒有才拒絕,不猜路徑:猜錯就是把待辦寫到一個沒有人會去讀的地方,而且看起來像成功。 實測抓到一個真的臭蟲並修掉。查找記錄原本寫成印出路徑、由呼叫端用命令替換接,於是裡面的錯誤退出只結束子行程、外層照樣往下走,訊息說欄位不合法卻回了找不到那一筆的碼,呼叫端拿到的碼與真正的原因不一樣。改寫成直接設變數再回碼。 這一輪只做存放與讀寫。登錄時的補問流程、事件詞彙表接上產生者、到期判定、逾期與失敗處理、提醒送到前景、wiki 雙向同步,六項都不在這一輪,但欄位都已經留在檔案裡,接的時候不必改存放格式。目前還沒有任何操作呼叫得到這六個子命令,行為契約只記下這一支工具存在,不讓清單先跑到技能前面去。
jsc-assist — 技能助理
jsc 技能組的 assist domain:技能與 hook 每跑一次就留下事件,助理負責把事件收攏、判斷健康狀態、寫進 wiki 的監控頁(MONITOR_{HASH})。
助理只做三件事:觸發既有技能、巡檢既有的唯讀腳本與狀態檔、把結果提醒給前景會話。它不做決策、不改程式碼、不覆寫 wiki。完整界線見 AGENTS.md 的「助理的界線」。
安裝、更新、移除
Marketplace 統一為 jsc(https://gitea.jsc.idv.tw/plugins/meta.git),安裝 token 為 jsc-assist@jsc。每個指令一行:
| CLI | 安裝 | 更新 | 移除 |
|---|---|---|---|
| claude | claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && claude plugin install jsc-assist@jsc |
claude plugin marketplace update jsc && claude plugin update jsc-assist@jsc |
claude plugin uninstall jsc-assist@jsc |
| codex | codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && codex plugin add jsc-assist@jsc |
codex plugin marketplace upgrade jsc |
codex plugin remove jsc-assist@jsc |
| copilot | copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && copilot plugin install jsc-assist@jsc |
copilot plugin marketplace update jsc && copilot plugin update jsc-assist@jsc |
copilot plugin uninstall jsc-assist@jsc |
| antigravity | git clone https://gitea.jsc.idv.tw/plugins/assist.git ~/plugins/assist && agy plugin install ~/plugins/assist |
git -C ~/plugins/assist pull && agy plugin uninstall jsc-assist && agy plugin install ~/plugins/assist |
agy plugin uninstall jsc-assist |
| kiro | kiro-cli plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && kiro-cli plugin install jsc-assist@jsc |
kiro-cli plugin marketplace update jsc && kiro-cli plugin update jsc-assist@jsc |
kiro-cli plugin uninstall jsc-assist@jsc |
antigravity 不支援 gitea URL 安裝,改用本地 clone 路徑。批次操作五個 CLI:使用
/jsc-cli:deploy。
Skills 目錄
呼叫方式:Claude / Antigravity /jsc-assist:{name};Codex ${name};Copilot / Kiro 描述需求自動觸發。
assistant
助理主體,四個操作:start 啟動、status 查現況、patrol 跑一輪巡檢、stop 停止。心跳的寫入、判定與清除一律交給 jsc-hooks 的 hooks/heartbeat.sh,判定只有那一份;系統排程一律交給 tools/schedule.sh;一輪巡檢的流程交給 tools/patrol.sh。工具一律用 current 那一組不帶版本的字面絕對路徑叫,不用技能提示給的快取基底目錄。根目錄由叫用文字的 工具根目錄= 帶進來,那一輪自己不解——權限比對指令的字面字串,帶未展開變數或波浪號的路徑一律要核准,解路徑的指令本身在無人值守時同樣被擋。規則與指令都必須是完整字面,路徑中段寫萬用字元不匹配,所以快取那組帶版本號的路徑放不進允許清單。心跳由巡檢寫,而且只由巡檢寫:一輪跑完、結果寫上監控頁了,才寫那一次心跳,所以心跳新鮮等於「上一輪巡檢真的做完了」。start 先跑一輪巡檢,再裝上巡檢那一筆排程;巡檢週期由心跳的過期門檻算出來,兩個數字綁在一起。patrol 讀五項來源(使用統計、版本與重啟閘門、SDLC 階段鎖與工作包鎖、心跳自述、執行狀態事件),各項各自獨立,一項掛掉其餘各項照跑、照記,結果寫上 MONITOR_{HASH}:那頁固定三塊,基本資料不動、最新一輪整塊換掉、摘要表保留近 24 輪,一輪一列。目錄頁 MONITOR_CONTENTS 在另一個存取庫(JSC_WIKI_REPO_CONTENTS),一台機器一個 H2 區塊,只更新自己那一個區塊,交給 jsc-gitea/tools/wiki-contents.sh upsert 寫,連結用絕對網址;那個存取庫沒設定時只少一筆索引,這一輪照樣算跑完、照樣寫心跳。status 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。stop 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
相依
| Plugin | 最低版本 | 用途 |
|---|---|---|
jsc-cli |
>=0.2.7 |
CLI 偵測與委派 |
jsc-gitea |
>=0.2.0 |
監控頁的所有 wiki 讀寫,一律經 tools/gitea.sh;目錄頁那一個區塊走 tools/wiki-contents.sh upsert,頁名雜湊走 tools/hash-id,兩頁要放進去的連結一律先過 tools/link-check.sh |
jsc-hooks |
>=0.3.7 |
心跳、閘門與事件來源($JSC_HOME 底下的狀態檔)。心跳的寫入、判定與清除一律走 hooks/heartbeat.sh,那支腳本是 0.3.7 才有的 |
jsc-log |
>=0.1.4 |
使用統計與工作日誌的資料來源 |
參考與工具
| 檔案 | 用途 |
|---|---|
tools/schedule.sh |
助理系統排程的安裝、移除與查現況。三個子命令 install、remove、status,只裝 patrol 這一筆——心跳由巡檢自己寫,install heartbeat 一律回 6,舊版遺留的心跳條目由 install patrol 順手清掉。巡檢週期由心跳的過期門檻算出來(2 × 週期 × 60 < 門檻,再取能整除一小時的分鐘數):門檻 300 秒是每 2 分鐘一輪,門檻 1800 秒是每 12 分鐘一輪。Linux、WSL 與 macOS 走 crontab,Windows 走 schtasks。條目行尾帶固定標記 # jsc-assist:assistant {工作},只動自己那一筆,別人的排程一行都不碰。條目自己把環境帶齊:CLI 用 command -v 解成絕對路徑、安裝當下把 GITEA_HOST、GITEA_TOKEN、JSC_HOME、JSC_ASSISTANT_HEARTBEAT_TTL 與已設定的 JSC_WIKI_REPO 系列快照進條目、自帶 JSC_GITEA_CONFIRM=yes。JSC_WIKI_REPO 系列含內容頁的 JSC_WIKI_REPO_MONITOR 與目錄頁的 JSC_WIKI_REPO_CONTENTS:監控頁 MONITOR_{HASH} 與目錄頁 MONITOR_CONTENTS 分屬不同存取庫,兩支變數都要帶。名單是安裝當下從環境撈出所有已設定的,不寫死,所以新增的頁型變數自動涵蓋,這支不必跟著改——cron 的 PATH 很短、不讀設定檔、也沒有 tty。印出條目時金鑰一律遮掉,條目本身含金鑰快照,crontab 檔案要保持只有本人讀得到,變數改過要重跑一次 install。安裝當下把解好的字面根目錄寫進條目的提示文字(工具根目錄={絕對路徑})並印成 patrol_root=:那一輪自己解不出根目錄,只能從提示文字拿,拿不到就停下回報;自訂巡檢指令沒帶這一段只警告、不中止。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron;也會檢查 $JSC_HOME/current 那組連結在不在、印出這一輪要開的 allow 規則,連結不在只警告、不代建。--dry-run 只印組出來的條目與寫回後的內容,什麼都不動 |
tools/patrol.sh |
一輪巡檢的收攏與收口。三個子命令:collect 取鎖、讀五項來源、組出監控頁的「最新一輪」與「近 24 輪摘要」兩塊、本輪的摘要列與目錄頁那一個區塊(區塊的 H2 標題是內容頁頁名 MONITOR_{HASH},upsert 拿標題當鍵;「監控頁」那一條是連結,網址留佔位,等監控頁寫成之後由呼叫端用 gitea.sh wiki-url 的絕對網址換掉);finish 在監控頁寫成之後才寫心跳、換上用量快照、放掉鎖;abort 只放掉鎖,不寫心跳。各項來源各自獨立,一項失敗其餘各項照跑,失敗那一項在頁上寫明是「這一項失敗」而不是沒資料。執行狀態事件那一項由 collect 自己叫 jsc-hooks/tools/report-status.sh 排空再輪替,把非 ok 的事件與「有 start 沒有配對 end」的技能彙整成頁上那一節;drain 是消耗性讀取,所以只由這支跑,且它失敗一律不中止那一輪。整輪拿一把目錄鎖,上一輪還在跑就回 4 讓開;鎖逾時(門檻取心跳門檻)會被下一輪搶回來,並在頁上記一筆。version-guard.sh report 回「查詢失敗」時照原字抄,不補查、不美化 |
references/behaviors.md |
本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 plugins/meta 的 references/guidelines.md「技能行為清單」 |
templates/monitor-contents.md |
目錄頁 MONITOR_CONTENTS 的範本,這一頁落在 JSC_WIKI_REPO_CONTENTS 解出的專用存取庫,和監控頁不同庫。版面是 H1、> 引言,然後一台機器一個 H2 區塊,欄位在標題底下一行一條 - {欄位名}:{值},頁上不放 markdown 表格。H2 標題就是內容頁頁名 MONITOR_{HASH},雜湊來源是 {主機名}/{登入帳號},主機名取短的那一段。寫入一律走 jsc-gitea/tools/wiki-contents.sh upsert,比對鍵是 H2 標題:只更新自己那一個區塊,別台機器的區塊原樣保留,禁止整頁覆蓋。「監控頁」那一條的連結一律寫成 [{頁名}]({絕對網址}),網址取 gitea.sh wiki-url 印的那一個,寫入前先過 jsc-gitea/tools/link-check.sh、結束碼 0 才寫;但那一條含主機位址與網址編碼,會變,所以不當鍵 |
templates/monitor-page.md |
內容頁 MONITOR_{HASH} 的範本。記的是這台機器的巡檢軌跡。頁面固定三塊:本頁基本資料建頁時寫一次就不動、最新一輪每輪整塊換掉、近 24 輪摘要一輪一列且最新的在最上面。軌跡留在摘要表,完整內容只留最新一輪,頁面才讀得完 |
助理的狀態檔
全部放在 $JSC_HOME/assistant/ 底下,一律純文字 key=value:
| 路徑 | 內容 |
|---|---|
heartbeat |
心跳檔,欄位 ts、pid、cli、session。只由 tools/patrol.sh finish 寫,也就是一輪巡檢跑完、結果記下來之後才寫。判準只看 ts,不看 pid 存活——五支 CLI 與容器裡的行程互相看不到彼此的 pid |
tasks/{id} |
待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
schedule.log |
排程條目的輸出。刻意放在存取庫外面:寫進專案會多出未追蹤檔,污染別人的變更盤點 |
patrol.lock/ |
一輪巡檢的鎖,是目錄——mkdir 是原子操作,搶不到就是別人在跑。裡面的 info 記 round、pid、started |
patrol/ |
本輪巡檢的暫存檔:latest.md 是「最新一輪」那一塊,summary.md 是摘要那一塊、裡面已經放好本輪這一列,summary-row.md 只有那一列,newpage.md 是頁不存在時要建的整頁,contents-entry.md 是目錄頁那一個 H2 區塊 |
usage-prev.tsv |
上一輪記下來的累計用量。有了它,下一輪的「本輪次數」才算得出來;沒有它的第一輪一律寫「-」,不拿累計冒充本輪 |