Commit Graph
13 Commits
Author SHA1 Message Date
jiantw83andClaude Opus 5 61f4a2dbc7 fix(assistant): 提醒佇列把內建項標成另一種列
實測踩到:這台機器的佇列八筆全是委派清單種入的內建項,動作是只提醒、
還沒有執行入口,所以每一輪都到期。於是每一個工作階段開頭固定吐八行
一模一樣的東西。

那不是提醒,是噪音——而讀佇列那一支自己的註解裡就寫著「對著一個刻意的
決定每個工作階段催一次,那是噪音不是提醒」。這一批就犯了那一條,只是
換成另一種來源。

分法照該有的處置切:使用者自己登錄的那一筆,人看到就做得了;內建項那
幾筆等的是接線,不是人。所以佇列的第一欄多一種值 builtin,讀的那一端
把前兩種逐筆點名、第三種收成一行總數。

種類在這一邊判,不在讀的那一邊:spec_key 只有這裡讀得到,而讀的那一端
的規則是只印不判。收尾多印一個 reminders_builtin=。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 13:39:45 +08:00
jiantw83andClaude Opus 5 932bfa826a feat(assistant): 一輪算完就寫提醒佇列
助理算得出哪幾筆到期、哪幾筆逾期,但那些結果只留在監控頁上。這一批讓
每一輪把「要送到人面前」的那幾筆寫成一份佇列檔,位置固定在助理狀態目錄
底下,讀的那一端只印、不判。

判定不放在讀的那一邊,理由是漂移:讓讀的人自己拿 due 欄與 next_run 去跟
現在比,就是第二套到期判定,跟這裡那一套遲早對不上,而對不上的那一天
兩邊都說自己是對的。

這樣換來一個新的失效模式,所以檔頭寫足四個值:輪次代號、UTC 時間戳、
連續失敗筆數、待辦總筆數,再加同一刻的 epoch 秒。助理沒在跑的時候佇列
不會更新,而一份舊佇列讀起來跟新的一模一樣——讀的那一端要算得出它多舊,
「沒有提醒」與「沒有人算提醒」不可以長得一樣。epoch 是為了讓讀的那一端
不必自己解 ISO 字串:那等於在每一個讀取端各放一份日期解析。

同一筆待辦既到期又逾期時只列逾期那一行。同一批提醒裡出現兩行會被讀成
兩件事,而「逾期五天」比「排定點過了」講得更清楚。

逾期那幾行的「逾期多久」直接取判定那一支算好的寫法,不在這裡再寫一個
時間長度格式化——同一個秒數在兩個地方就會印出兩種說法。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:16:57 +08:00
jiantw83andClaude Opus 5 465bd9d40a feat(assistant): 逾期判定接上來,逾期與連續失敗每一輪都提
逾期本來沒實作,監控頁上就寫著「上面的判定只講到期,不講逾期」。

到期與逾期分兩欄,不合成一個判定:到期是「該跑了」,逾期是「due 欄那個
截止時間過了」。一筆七天一次的檢查項天天都可能到期,卻永遠不會逾期;
一筆有截止時間的交辦可能早就逾期,卻因為排定點還沒到而判成還沒到。
合成一個值就把兩種情況混成同一格。

due.sh 多第十二欄 overdue,值是減號(沒有截止時間)、no、逾期的秒數,
或 bad。bad 算成欄位不合法回 2——一個解不開的截止時間跟沒有截止時間
在畫面上長得一模一樣,而前者是登錄的時候就填錯了。done 那幾筆不算逾期;
被暫停的那幾筆照算,因為暫停中止的是到期判定,不是日曆,而一筆被暫停
又逾期的交辦正好是最容易被忘掉的那一種。

另加唯讀子命令 due.sh overdue 給狀態查詢那一路走。走 scan 會推進快照,
那一輪之後的事件判定會全部落空;走 events 又判不到逾期;讓呼叫端自己
拿 due 欄去比更糟,那是第二套判定,漂移的那一天兩邊都說自己對。

patrol.sh 在逾期或連續失敗筆數大於 0 時各列一筆待人處理,每一輪都列。
兩種都不會自己好:截止時間過了不會因為過更久就不逾期,連續失敗的項目
每一輪都會再試一次然後再失敗一次。「提過了」不是「處理過了」,而助理
不替人按暫停,那個狀態留給人設。

監控頁那一節逐筆點名連續失敗的那幾筆——id、失敗幾次、最後一次執行、
標題。原本只印一個總數,而失敗次數這個欄位存在的理由是指出「有一筆壞掉
的項目每輪重試而沒人知道」,不說出是哪一筆,那個理由只完成一半。

目錄頁刻意不加欄位。那一頁一台機器一個區塊、各自那一輪寫,新加一條只有
跑到新版的機器寫得出來,讀的人分不出「這台沒有逾期」與「這台還沒寫這
一條」——理由與當初把「本輪非 ok 事件數」擋在那一頁外面的一樣。

欄位數從 11 變 12,run-due.sh 那道欄位數檢核跟著改成 12。那道檢核擋的
不是第 12 欄本身,是「兩支不是同一版」——版本不同的時候,前 11 欄的
順序也不能假設還是原來那樣。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:53:00 +08:00
jiantw83andClaude Opus 5 ccb28e2bb3 fix(執行): 到期清單的路徑一律由呼叫端餵進來,並拒跑過舊的清單
判到期與執行是兩支腳本,中間靠一個檔案交棒。判到期那一支是被巡檢用 --out
叫的,輸出寫進那一輪自己的暫存目錄;而執行那一支留了一個預設路徑,指向助理
狀態目錄底下那一份。兩邊不是同一個位置。

於是執行那一支從上線到現在,每一輪讀的都是「上一次有人手動跑判定留下的那一
份」。實測那份清單在機器上放了 67 小時,每一輪都被拿去動手,其中一輪還跑了
一筆已經被移除的待辦——那一筆的識別碼在重新種入時換過了。

而它看起來完全正常。那份舊清單產生的當下是對的,內容剛好沒變,所以每一輪的
報告都合理。錯了 67 小時才顯形。

兩處改動。判到期那一步本來只把給人看的那個檔案轉出去,機器可讀的那一份被
吞掉了,現在一起印成 due_rows_file=。執行那一支拿掉預設值:少帶那個選項就
回用法錯誤,吵一次總比安靜地對舊資料動手好。

另外加一道時效保護。判到期與執行之間隔著呼叫端,那一段有可能斷掉——判定失敗
了而那一輪照樣往下走,或呼叫端餵進上一輪的路徑。門檻取心跳的過期門檻:那是
這台機器認定「一輪跑完的紀錄還算新鮮」的長度,一份比它還舊的到期清單本來就
不可能是這一輪產生的。讀不到門檻就退回 300 秒,跟心跳那一支的預設一致。

實測:不帶選項回 6,清單不存在回 2,兩小時前的清單回 2 且訊息帶出實際秒數與
門檻,新的空清單回 0;拿真機那份 67 小時前的舊清單餵進去,確實被擋下。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 08:59:43 +08:00
jiantw83andClaude Opus 5 ebce7fc0c3 feat(巡檢): 監控頁三塊的接法收進腳本,新增 rebuild 子命令
接三塊這件事本來留給呼叫端,而呼叫端是一個代理人,於是它每一輪臨場挑工具。
實測某一輪挑了 awk,兩次都被權限擋掉;那一輪有人在場所以它改用別的方式繞過
去了,並且自己在報告裡指出,無人值守那一輪遇到同一個擋法會直接卡住——而鎖
在第一步就取走了,接下來每一輪都會讓開到鎖逾時。

更根本的證據在暫存目錄:實測累積了 124 個檔案,名字是 newpage.md、new-page.md、
new_page.md、page-new.md、monitor-new.md 這種。同一件事每輪重做一次,每輪換一
組檔名。規格說那裡只該有五個檔案。

接法只有一種對的形狀,那就不該放在提示裡。收進腳本之後那一步不再需要任何
通用文字工具,也就沒有東西可以被擋。

基本資料那一塊逐字照抄,連尾端空行都不動。第一版剝掉尾端空行再補一個,內容
沒少但技能本文寫的是「逐字保留」,那句話就變成假的了——實測比對出差一個空行
之後改成只在它沒有以空行收尾時才補分隔。

舊格式頁由腳本自己認:沒有「最新一輪」那個標題就留基本資料、丟掉堆疊的各輪
小節、摘要表從本輪開始,並印 old_format=yes converted=1,不讓呼叫端自己判。

實測:拿這台機器真的監控頁跑,三個區塊、摘要 24 列、舊列留 23、基本資料逐字
相同;舊格式頁轉成三區塊且摘要從本輪開始;把結果再當舊頁餵一次冪等;沒帶
--old、舊頁檔不存在、缺同輪產出的區塊三種都回 6。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:42:54 +08:00
jiantw83 737fcf13fd feat(due): 事件偵測與到期判定,七個事件名接上狀態來源
待辦簿存得住 trigger 與 recur,但沒有東西判「現在到期了沒有」。存放那一支的檔頭明寫它不判定、算的邏輯在別的地方,這一輪就是補那個別的地方。

另開一支而不併進存放那一支。那一支的檔頭把「我不判定」寫成合約,而合約是呼叫端唯一的依據;併進去就要推翻那整段話,而讀過舊版的人會以為那一支不判、自己再判一次,於是漂移從註解裡長出來。界線做成單向:判定這一支只讀待辦檔與印值,要改狀態一律回頭叫存放那一支的完成與失敗。

事件偵測靠比對狀態快照,不改任何產生者。每一輪讀那幾支產生者留下的狀態,跟上一輪存下的快照比,有差別就算事件發生。六個事件名各自接上本機狀態來源:工作包鎖檔轉態、階段檔換值、錯誤紀錄行數變多、工作階段開始與結束、日誌暫存區清空。

這個選擇有一個明確的代價,寫進檔頭:兩輪之間發生又消失的事件會漏掉。往後誰假設「事件不會漏」就會出錯,而那種錯是無聲的。不為了補這個洞去改那幾支產生者——要它們各自在事件發生那一刻多寫一筆,任何一支忘了寫就讓對應的事件從此不再發生,而那同樣無聲,還散在五個 domain 裡查不出是誰漏的。快照比對至少壞掉時看得出來。

第一次跑一律只建快照、不發事件,否則機器上所有既有狀態都會被當成剛發生,讓每一筆事件型待辦一次全到期。同一條理由逐個來源再套一次:某個來源上一輪不存在、這一輪才出現,那個來源這一輪也只建快照。所以快照要留一列記每個來源上一輪是看得到還是不存在——少了它就分不出「上一輪沒有這個來源」與「上一輪這個來源是空的」,而只有後者的差別才算事件。來源目錄從有變成不見時也不發事件,只記警示:整個目錄被砍掉不是每一支工作包都合併了。

四種組合一個都不壓。時鐘型的排定點從那個時間往後數、一次加一個間隔,不從上次執行起算——每次拿上次執行加間隔,執行慢幾秒就往後挪幾秒,一天下來就偏掉。漏掉的排定點只補一次不逐個追補,否則機器關一天再開會連續跑幾十輪同一件事。事件型配間隔時,那個間隔照存放那一支的定義當「重新武裝的最短間隔」。

事件型的下次執行時間一律留空。事件型沒有預定時間,唯一算得出來的是重新武裝時刻,但那個值填進去會被狀態表與提醒讀成「那個時間會跑」,它其實只是「那個時間之前不會跑」,差別在畫面上看不出來。那個值改印成另一個欄位,要看的人看得到。

規格有兩條在同一個欄位上打架。同一件事不重複觸發的判準是「事件發生時間比上次執行晚」,但失敗也會寫上次執行,於是失敗過的事件型待辦再也等不到同一次事件,也就永遠不重試——而規格明確要求失敗照重試。改用連續失敗次數當第三個判準把兩者分開:狀態還是待辦而次數大於零,代表上一次執行失敗,一律算到期,不再比事件也不看排定點。完成會把次數歸零,所以成功那一次不會被誤判成要重試。

兩項刻意標成未接線並吵出來,不假裝算得出來。分析完成那一個事件的狀態來源在 wiki 上、要連網要金鑰,混進來之後金鑰一過期就讓事件偵測整項每輪失敗;更糟的是判不出來時那幾筆會靜靜地永遠不到期,跟填一個不存在的事件名一樣看不出壞在哪。cron 式子同理,五個欄位的萬用字元與步進算錯一格就是差一小時或差一天,而那種錯要等真的跑錯才看得出來。兩者的處置都是印一行未接線、列進要人接手那一節,明說「這一筆現在判不出到期,不是還沒發生」。

間隔格式規格從來沒定義過,這一輪定成正整數加單位,刻意不收裸數字——排程週期以分鐘算、心跳門檻以秒算,裸數字兩種都讀得通,讀錯就差六十倍。

巡檢那一路一併接上:快照要有人推進才活得起來。刻意不把它算進既有五項的成敗,避免動到摘要表的欄名與本輪項目那一列的文字,舊頁那些列的欄名不會跟著改,同一張表就會有兩種寫法。失敗時改成記警示與待人處理。

順手修掉一個同類的坑:用冒號當空指令去清空檔案時,重導向失敗會讓整個殼直接結束,後面的分流一次都跑不到。這一支一律改用 true 並把理由寫進註解。
2026-09-03 16:37:44 +08:00
jiantw83 d8aa0b1f5d feat(patrol): 巡檢改組目錄頁區塊檔,輸出檔改名 contents-entry.md
一輪巡檢組給目錄頁的輸出,從一列 markdown 表格改成一整個大標題區塊:
`## {監控頁頁名}` 那一行、空行,然後每個欄位一條 `- {欄位名}:{值}`。輸出
檔跟著從 `contents.tsv` 改名 `contents-entry.md`,`contents_file=` 指向
新檔名。

目錄頁已經不放表格,再組出那一列就沒有地方放。舊檔名說的是逐欄對照的資料
檔,內容其實是一段 markdown,名不對實會讓呼叫端以為還能逐欄解析。改名讓
呼叫端契約的變動在檔名上就看得見,而不是同名檔悄悄換了格式——同名換格式
的話,還沒跟上的呼叫端會拿到讀得開卻意義全錯的內容。

新增 `oneline` 處理條列一條的值:換行一律壓成空白,因為一條條列裡的換行會
被讀成另一條,或者讓整個區塊提早結束;`|` 反而不跳脫,條列裡沒有切欄的
意思,跳脫過的符號會原樣顯示在頁面上。監控頁那一條的網址仍然留佔位,由
呼叫端換成真網址、驗過連得到才寫,這支腳本一頁都不寫。裸雜湊照樣留一條,
讓人不必從標題切字串就抄得到。

範圍是一輪巡檢給目錄頁的輸出檔。
2026-09-02 17:21:23 +08:00
jiantw83 3dac1475df feat(狀態回報): 收尾寫一筆 skill-end 事件
現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

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

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
2026-09-02 16:01:13 +08:00
jiantw83 9ea83da38b feat(link): 連結一律寫成 [文字](絕對網址),寫入前先驗證連得到
取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與
「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上
看起來像普通文字或死連結,巡不到也修不了。

連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API,
不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把
好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁
整批判死。
2026-09-02 14:27:18 +08:00
jiantw83 4bf99dde76 feat(wiki): 監控目錄頁改走專用存取庫,比對鍵改用裸 HASH
What:MONITOR_CONTENTS 改由 wiki-repo CONTENTS 解析並透過 wiki-contents.sh
upsert 寫入,MONITOR_{HASH} 仍走 wiki-repo MONITOR。目錄頁新增一欄裸 HASH 當
比對鍵,第一欄的絕對網址連結留給人看。主機名一律取短名。

Why:目錄頁集中之後兩頁不再同庫,[[...]] 只在同一個 wiki 內解析,連結必須改絕對
網址。比對鍵原本是含網址的儲存格,換主機、換存取庫或 URL 編碼有差就比對不到,
巡檢每 15 分鐘寫一次,重複列會累積得很快。

How:目錄頁解不出存取庫時不再中止巡檢——那一頁只是索引,內容頁寫成就照樣寫心跳。
內容頁寫不成仍然中止且不寫心跳,自咬環的前提沒有鬆動。

Who:jsc-assist
2026-09-02 11:01:51 +08:00
jiantw83 02bfc70f4e fix(assistant): 讓執行路徑走錯與警示原因都看得見
兩支腳本開跑就檢查自己的執行路徑,監控頁的摘要表與最新一輪那一塊補上
「警示來源」。

兩件都是實測之後補的。用全新的 CLI 行程跑,代理人會把工具路徑解回外掛
快取那一份,不是指定的工作樹那一份。原本的對策只在技能說明書裡寫明禁
止,那是文件約束,擋不住真的走錯的那一輪。權限閘門只放行 current 那一
組確切路徑,走錯就被靜靜擋掉,那一輪不寫心跳,外面只看得到心跳過期。
另一頭,沙箱跑出來的摘要列是「警示、四項全過、待人處理 0」,讀的人看
不出警示哪來。查過不是缺陷:四項來源都讀得到,是讀到的內容有警示。摘
要表的用途本來就是一眼看出從哪一輪開始壞,少了原因那一欄就做不到。

巡檢與排程兩支腳本開跑時先比對自己是不是從 current 底下被叫起來的,不
是就往 stderr 印一行警告,點名實際路徑、應該用的路徑與理由。刻意只警告
不中止:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉,真
正的失敗本來就發生在權限閘門那裡。摘要表從四欄加到五欄,最新一輪那一
塊也補一列,值取每一處設警示時收下來的短理由,多個用頓號串,沒有就寫
「無」。腳本裡每一處把判定改成警示的地方,一律改走同一支函式順手收下
理由,collect 的輸出多印一行 warn_sources。技能主文、行為清單與監控頁
範本跟著寫明這兩件事。

功能範圍是助理的一輪巡檢與系統排程安裝。
2026-09-01 18:54:05 +08:00
jiantw83 717a562cb7 refactor(patrol): 監控頁改成固定三塊
巡檢產出與監控頁範本改成固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。

舊寫法是一輪附加一節。十五分鐘跑一輪,一天就疊出九十六節,那樣的頁沒
有人讀得完,也就沒有人會發現助理壞在哪一輪。軌跡留著卻沒人看,等於沒
有留。

基本資料建頁時寫一次就不動。最新一輪每輪整塊換掉,只留最新那一輪的完
整內容。摘要表一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列,
一列只有四欄:巡檢時間、本輪判定、四項成敗、待人處理筆數,一眼看得出
是從哪一輪開始壞的。collect 的輸出鍵位跟著換,section_file 不再產出,
改印 latest_file、summary_file 與 summary_row_file,另外多印本輪待人處
理的筆數。範本連帶把流程圖與寫入規則改成先讀回舊頁、再三塊重組整頁寫
回,並寫明舊格式的頁第一次重組要怎麼收。

順手把腳本內部找別的 domain 工具的順序改成 current 連結優先,理由跟排
程那支一樣:技能與權限規則都以 current 為準,腳本自己去挑另一個版本就
會跑到混版的工具,而那種不一致查起來沒有線索。功能範圍是一輪巡檢的收
攏與監控頁產出。
2026-09-01 18:28:04 +08:00
jiantw83 2b7176a644 feat(patrol): 一輪巡檢落地,心跳改由巡檢跑完才寫
What:
- 新增 tools/patrol.sh,三個子命令:collect 收集、finish 收尾、abort 中止。這一輪的巡檢項目是四項純讀取:使用統計、版本落差與重啟閘門、階段鎖與工作包鎖、心跳自述。
- 心跳的寫入從排程移到巡檢收尾。排程只呼叫巡檢,不再直接寫心跳。
- 排程週期由心跳門檻推導,不再寫死。install heartbeat 這個工作代號改為拒絕。

Why:
- 排程直接寫心跳的話,心跳新鮮只證明排程活著。巡檢整個壞掉、每輪都失敗,心跳照樣新鮮,閘門照樣放行,而且沒有任何錯誤訊息——這是無聲失效,是最難發現的一種。
- 改成巡檢寫,心跳新鮮才等於上一輪真的跑完了。閘門判的才是工作訊號,不是行程存活訊號。
- 門檻是讀取端的設定,心跳檔裡不存它。所以週期與門檻各寫死一個數字一定會撞:門檻五分鐘、巡檢十五分鐘,心跳永遠是過期的。

How:
- 週期取「滿足漏掉一輪還算新鮮、漏掉兩輪才過期」的最大值,並且要能整除一小時。要拉長巡檢週期就調大門檻,週期自動跟著長,兩個數字不會各走各的。
- 心跳只看「這一輪有沒有把結果記下來」,不看四項的成敗。四項有失敗但監控頁寫成了就寫心跳,頁上判定標警示;頁寫不成就中止,一定不寫,讓心跳自己過期。頁每輪都寫失敗卻照樣寫心跳,等於把這次要修掉的缺陷原封不動搬過去。
- 整輪拿一把目錄鎖,搶不到就讓開,不排隊也不並行。並行兩輪會在同一頁附兩節,還會互相蓋掉用量快照。鎖逾時可被下一輪搶走,被搶走的那一輪收尾時對不上就不寫心跳——它沒跑到底,不該蓋章。
- 任一項失敗不影響其餘項目。失敗要在監控頁上看得出來是失敗,不是沒資料;來源是空的則明寫「那是零次,不是故障」。
- 版本盤點照抄腳本原字。抄到查詢失敗就寫查詢失敗,不改寫成相符、也不自己補查遠端版本——查不到是沒有證據,不是版本沒問題。

Who:
助理落地的第四塊。排程那一輪留下的問題在這裡解掉了。
2026-09-01 15:06:10 +08:00