委派清單補上第十二欄 probe 之後,這一支不再把所有非 invoke 的列一律推成
只提醒:填了指令的四列改成把那一行指令代好代入點寫進動作欄,寫著
pending 的七列照舊只提醒但另外印出來,讓「入口還沒接上」跟「本來就只提醒」
在回報上分得開。
三個判斷刻意寫死:
一、way 含 invoke 的列連看都不看 probe。填了指令會讓整支技能的交出變成
只跑一支腳本,那支技能該寫的頁一頁都不會寫,而且看起來完全正常。
二、probe 代不進去一律退回只提醒並照樣種入。不種入在 apply 那一路等於
移除,於是上游一格填錯就會刪掉一筆帶著 last_run 與 fail_count 的內建項。
金錢符號與波浪號擋在種入這一刻,那兩種寫法在無人值守那一輪解不出來、
也進不了允許清單,會被靜靜擋掉。
三、{cli} 與 {repo} 留在值裡不展開。種入的當下還不知道要代什麼,展開成
多筆會讓同一個 spec_key 有好幾個檔案,一致化整個垮掉。展開由執行那一步
負責,而動作欄裡出現大括號就是還沒代好,一律不得原樣拿去執行。
清單只有十一欄時整份先數一次欄位數,全部照舊推成只提醒、只印一行說明,
行為與加這一欄之前一模一樣。第十三欄以後另接一個收尾變數,免得上游哪天
加一欄就把多出來的值黏進 probe,代入點檢查全過得了關、最後執行的卻是
一行誰都沒寫過的指令。
相依下限刻意不動。清單那一欄晚一步到也照常跑得完,把下限拉到有那一欄的
版本等於逼兩個存放庫排合併順序,而排順序正是這一段程式碼要免掉的事。
三份 manifest 的版號從 0.1.9 升到 0.2.0。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
714 lines
40 KiB
Bash
Executable File
714 lines
40 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# seed-tasks.sh — 依委派清單種入與重建助理的內建定期檢查項(供 jsc-assist:assistant 呼叫)。
|
||
#
|
||
# 用法:
|
||
# seed-tasks.sh plan [--root {字面絕對路徑}] [--spec {清單檔}]
|
||
# [--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||
# seed-tasks.sh apply [--root {字面絕對路徑}] [--spec {清單檔}]
|
||
# [--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||
#
|
||
# plan 唯讀預覽:算出該加哪幾筆、該移除哪幾筆、哪幾筆對不上,一筆都不寫。
|
||
# apply 真的做:該加的用 tasks.sh add 加,該移除的用 tasks.sh remove 移除。
|
||
#
|
||
# 結束碼:
|
||
# 0 對齊完成。plan 印完,或 apply 做完。零筆改動照樣是 0:清單與待辦簿本來就一致,
|
||
# 不是失敗,也不必分流
|
||
# 1 委派清單讀不到:找不到那個檔案,或檔案在卻讀不到內容。**一筆都不動**
|
||
# 2 清單讀到了,卻解不出任何一列可交項目。**一筆都不動**,理由見下面「空清單一律不動手」
|
||
# 3 找不到本 domain 的 tasks.sh。**一筆都不動**:待辦簿只有那一支寫得出來
|
||
# 4 至少一筆 add 或 remove 失敗,其餘各筆照做完。哪一筆失敗、回了哪一碼,逐筆印出來
|
||
# 5 檔案系統失敗:暫存目錄建不起來,或暫存檔寫不進去
|
||
# 6 用法錯誤:不認得的子命令、不認得的選項、選項缺值、--allow-cond 的值形狀不對
|
||
#
|
||
# --- 這一支負責什麼、不負責什麼 ---
|
||
#
|
||
# 只負責「待辦簿裡的內建項要跟委派清單一致」這一件事。它不判到期(那是 due.sh)、不執行任何
|
||
# 一筆待辦、不寫待辦檔(那是 tasks.sh,那一支是唯一的寫入入口,這裡一律去呼叫它,不自己動
|
||
# tasks/ 底下的檔案)。
|
||
# 種入與重建是同一段程式,不是兩段。理由:兩段程式各自回答「待辦簿裡該有哪幾筆」這個同一個
|
||
# 問題,第一次答案相同看起來沒事,等其中一段改了判準,兩段就會一邊加一邊刪同一筆,每一輪
|
||
# 反覆。一段程式做到冪等,第一次跑就是種入、之後每一次跑都是重建,行為只有一種。
|
||
# 所以這一支不記「跑過沒有」。要不要動手完全由現況決定:清單上有、待辦簿沒有就加;待辦簿有、
|
||
# 清單上沒有就移除;兩邊都有就留著不動。第二次跑因此什麼都不會加。
|
||
#
|
||
# --- 誰是同一筆:靠 spec_key,不靠 id 也不靠標題 ---
|
||
#
|
||
# 待辦簿的 id 是「建立時間加標題」的雜湊,跟技能名沒有關係,所以反查不了。反查鍵是待辦檔裡
|
||
# 的 spec_key 欄位,值是 jsc-{domain}:{技能名},由 tasks.sh add 的 --spec-key 寫進去。
|
||
# 完整理由寫在 tasks.sh 的檔頭「spec_key 為什麼非有不可」,這裡只記結論:拿標題當鍵會讓
|
||
# 標題的措辭變成介面,拿 action 當鍵會讓提醒類的那十幾筆全部撞在一起,也會撞上使用者自己
|
||
# 交辦、動作剛好是同一支技能的那一筆。
|
||
#
|
||
# --- 哪些欄位從清單來、哪些自己補 ---
|
||
#
|
||
# 清單有十二欄,待辦簿有十五個鍵,直接抄過來的只有兩欄。逐個交代:
|
||
# 從清單直接抄過來
|
||
# trigger 原樣抄。第一次什麼時候到期是判定結果,不是這一支的決定
|
||
# recur 原樣抄。同上
|
||
# 從清單推出來
|
||
# spec_key domain 與 name 兩欄合起來寫成 jsc-{domain}:{技能名},那就是一支技能的身分
|
||
# action 由 way 與 probe 兩欄一起推,對映見下一段。way 與 action 不是同一件事
|
||
# title 固定寫成「委派清單內建項:{spec_key}」。標題刻意不含 way、trigger、recur:
|
||
# 那幾欄會隨清單改動而變,寫進標題就等於每一次改動都換一個 id,而 id 一換,
|
||
# last_run 與 fail_count 的歷史就跟著斷掉
|
||
# 這一支自己補,清單裡沒有對應欄位
|
||
# kind 一律 check。清單管的是「定期做的事」,交辦事項是使用者當面給的,不從清單來
|
||
# origin 一律 assistant。這一欄決定重建時動不動它,所以不能空、也不能是 user
|
||
# repo 一律留空。這幾項是機器層級的檢查,不綁單一存取庫;要掃存取庫的那幾項由助理
|
||
# 自己掃工作目錄底下所有存取庫,不逐筆綁路徑
|
||
# due 一律留空。清單沒有截止時間這一欄,補一個猜出來的截止時間會讓監控頁標出一批
|
||
# 沒有人約定過的逾期。留空是合法狀態,意思是沒有截止時間
|
||
# 不抄過來的清單欄位
|
||
# verdict 只用來決定要不要種入,見下面「條件式交的那幾支一律不種入」
|
||
# slice、human 是給人讀的判定說明,逐字抄進標題會讓標題長到在狀態表上看不完;要看它們
|
||
# 就去看清單本身
|
||
# next 跑完之後建議接哪一支,那是建議下一個指令那一項要用的資料,跟排程無關
|
||
# version、origin 清單自己的稽核欄位。清單的 origin 是 seed 或 judged,講的是「這一列
|
||
# 判定過沒有」;待辦簿的 origin 是 user 或 assistant,講的是「這一筆誰交辦的」。
|
||
# 兩個同名不同義,一律不互抄
|
||
# state、last_run、next_run、fail_count 由 tasks.sh 與判到期那一邊維護,這一支不碰。
|
||
#
|
||
# --- way 與 probe 怎麼推出 action ---
|
||
#
|
||
# 清單的 way 是交出方式,三種:invoke 觸發、patrol 巡檢、remind 提醒。待辦簿的 action 是
|
||
# 助理實際要跑的事,三種:技能名、指令、remind。兩套詞彙不對應,所以要明寫對映:
|
||
# way 含 invoke → action 取技能名,probe 一律不看。觸發的定義就是呼叫既有技能、內容
|
||
# 照那支技能自己的流程走,所以填技能名就是照判定結果做
|
||
# 其餘那幾種(patrol、remind、patrol,remind)看 probe 那一欄:
|
||
# probe 是一行指令 → action 取那一行指令,代入點先代好
|
||
# probe 是 pending:{理由} → action 取 remind,另外印一行 pending=
|
||
# probe 是減號或空的 → action 取 remind
|
||
# 中間那一條原本沒有:這一支以前把非 invoke 的列一律推成 remind,因為那些唯讀切片沒有獨立
|
||
# 的入口——巡檢那一輪讀的是固定那幾項來源,沒有一支腳本代表得了「某一支技能的唯讀切片」。
|
||
# 清單補上 probe 之後,有入口的那幾列指得出來了,所以改成照 probe 走。沒有入口的那幾列行為
|
||
# 一個字都沒變,還是 remind:填技能名會讓助理下一輪去呼叫整支技能,那正是切片交要防的事,
|
||
# 留在人手上的那一半會被一路跑完。
|
||
# invoke 那一路為什麼連看都不看 probe:填了指令會讓整支交出變成只跑一支腳本,那支技能該寫
|
||
# 的頁一頁都不會寫,而且看起來完全正常。清單自己的檔頭也明寫 invoke 的列一律填減號,所以
|
||
# 那是清單填錯,不是這裡要順從的設定。這一支照 way 取技能名、另外印一行 probe_bad= 報出來,
|
||
# 不因為那個錯就不種入——不種入等於讓上游一個打錯的欄位把一筆好好的內建項刪掉。
|
||
#
|
||
# --- probe 代不進去的時候一律退回 remind ---
|
||
#
|
||
# probe 有問題的處置只有一種:**退回 remind,另外印一行 probe_bad=,照樣種入那一筆。**
|
||
# 涵蓋這幾種:路徑不是 {root}/jsc-{domain}/ 開頭、指令裡有金錢符號或波浪號(那兩種在無人
|
||
# 值守那一輪解不出來,也進不了允許清單)、出現 {root}、{cli}、{repo} 以外的代入點、代不出
|
||
# 根目錄,還有那支腳本不在這台機器上。
|
||
# 為什麼不改成「不種入」:不種入在 apply 那一路等於移除,於是上游改壞一格就會把一筆帶著
|
||
# last_run 與 fail_count 的內建項刪掉。退回 remind 的代價只是那一筆這一輪不動手,人照樣被
|
||
# 提醒該做那一段,而 probe_bad= 那幾行指得出是哪一支、壞在哪。
|
||
# 為什麼不把代不出來的指令原樣種進去:那個值最後會被拿去執行。帶著 {root} 或 {cli} 的字面值
|
||
# 執行起來就是指到一個不存在的路徑,每一輪失敗一次;帶著金錢符號的那一種更糟,展開之後跑到
|
||
# 哪裡去沒有人說得準。
|
||
#
|
||
# --- {cli} 與 {repo} 留在值裡,執行那一步才代 ---
|
||
#
|
||
# probe 認得三個代入點。{root} 這一支自己代得掉,另外兩個代不掉:{cli} 要代入助理偵測到的
|
||
# CLI 代號,{repo} 要代入助理掃到的存取庫工作目錄,兩件事種入的當下都還不知道。
|
||
# 兩條路,這裡選第二條:
|
||
# 一、種入時就展開成多筆。代價是一致化整個垮掉。反查鍵 spec_key 是一支技能一個值,展開成
|
||
# 多筆就是同一個鍵有好幾個檔案,而這一支碰到同一個鍵有兩筆以上一律不動它、只印 dup=,
|
||
# 於是每一輪都印一堆 dup=、一筆都對不齊。而且 CLI 裝了新的一支、存取庫多 clone 一個,
|
||
# 那組展開就過期了,要重新展開就得先移除再重新登錄,last_run 與 fail_count 跟著歸零,
|
||
# 那正是下面 drift 那一段刻意不做的事。
|
||
# 二、留著代入點,執行那一步再展開。代價是待辦簿裡留著一個還沒代進去的字面值。
|
||
# 選二,並且把代價擋掉:**action 裡出現大括號就是還沒代好的代入點,一律不得原樣拿去執行。**
|
||
# 執行那一步(現在還沒接上來,見技能本文)要先把 {cli} 換成每一支偵測到的 CLI 代號、把
|
||
# {repo} 換成每一個掃到的存取庫工作目錄,一個目標跑一次,那一輪所有目標的結果合起來算這一筆
|
||
# 的一次成敗。展開由執行那一步負責,不由這一支、也不由 tasks.sh 負責。
|
||
# 其他讀取端會不會誤解,逐個交代:tasks.sh 只存放,action 對它是不透明的一個字串;due.sh
|
||
# 只判到期,它讀 action 是為了印出來,一次都不執行;技能的 status 與監控頁也只是把 action
|
||
# 原樣印出來。三個讀取端沒有一個會拿 action 去跑,所以現在留著代入點不會有人跑錯;真正要防
|
||
# 的是往後接執行那一步的人,那一條規則寫在上面那一句,也寫進技能本文與行為清單。
|
||
# 種入時每一筆指令型都印一行 probe=,holes= 那一欄列出這一筆還留著哪幾個代入點,所以留了
|
||
# 什麼看得見,不必去讀待辦檔才知道。
|
||
#
|
||
# --- pending 的那幾筆只印不寫 ---
|
||
#
|
||
# probe 是 pending:{理由} 的那幾列,action 照舊取 remind,但要跟「本來就只提醒」分得開。
|
||
# 分法是**只印在回報裡**:一行 pending={spec_key} reason={清單上的理由原文},摘要另外印
|
||
# pending= 的筆數。待辦檔本身一個字都不加。
|
||
# 為什麼不寫進標題:標題是 id 的雜湊來源,改標題就換 id,last_run 與 fail_count 跟著斷掉;
|
||
# 而且這一支的漂移比對只比 kind、action、trigger、recur 四欄,不比標題,所以一個寫進標題的
|
||
# 記號在 pending 換成指令、或換成減號的時候不會被比出來,會一直留在那裡指著一件已經不成立
|
||
# 的事。
|
||
# 為什麼不另立一個欄位:那是待辦簿的存放格式,十五個鍵是固定的,加一個鍵要動 tasks.sh 的
|
||
# 寫入、驗證、list 欄位順序,還有每一個讀 list 的人。而 pending 的理由是清單上的散文,會隨
|
||
# 上游改,待辦簿又沒有 edit 操作,抄進去就是抄了一份改不掉的舊值。清單的 verdict、slice、
|
||
# human 幾欄不抄進待辦簿也是同一個理由。
|
||
# 只印在回報裡夠不夠用:夠。人要看的入口就是 status,那一個操作跑的是這一支的 plan,
|
||
# pending= 與 held=、drift= 印在同一個地方,一次看完。
|
||
#
|
||
# --- 清單只有十一欄的時候 ---
|
||
#
|
||
# probe 是清單的第十二欄,比這一支晚不了也早不了:兩個存取庫各自合併,總有一邊先到。所以
|
||
# 十一欄的舊清單餵進來要照常跑完,行為與加上這一欄之前一模一樣。
|
||
# 判法是先數一次資料列的欄位數,取最大值當這一份清單的形狀:
|
||
# 最大欄位數 12 以上 → 這一份有 probe,照上面那幾段走;某一列少一格導致 probe 是空的,
|
||
# 那是那一列漏填,退回 remind 並印 probe_bad=
|
||
# 最大欄位數 11 以下 → 這一份沒有 probe,全部照 way 推 action,一行 note 講明這一輪為什麼
|
||
# 沒有任何一筆指令型,不逐列印 probe_bad=——三十五行同一個原因的警告
|
||
# 會把真的缺失蓋掉
|
||
# 取最大值而不是逐列各判:清單是一份檔案,欄位數是整份的性質。逐列各判的話,十二欄清單裡
|
||
# 一列漏填會被當成「這一列是舊格式」而靜靜放過,那正是要抓出來的東西。
|
||
# 第十三欄以後留給往後:讀的時候另外接一個變數收尾,多出來的欄位不會被黏進 probe。少了這個
|
||
# 收尾,上游哪天加第十三欄,probe 讀到的就是「指令加一個定位字元加第十三欄」,代入點檢查
|
||
# 全部過得了關,最後拿去執行的是一行誰都沒寫過的指令。
|
||
#
|
||
# --- 條件式交的那幾支一律不種入 ---
|
||
#
|
||
# 判定是 cond 的那幾列,預設一筆都不種入,並且逐列印一行 held=,把清單上的條件原文帶出來。
|
||
# 理由是條件本身是散文,程式判不了。清單裡沒有一個機器讀得懂的條件欄位,只有 slice 與 human
|
||
# 兩欄的說明文字,所以「條件成立了沒有」這件事現在只有人答得出來。
|
||
# 預設種入的代價已經有現成的例子:其中一支健檢技能的條件明寫「只有在它自家路徑不再帶版本號
|
||
# 之後才可以交」,而那個條件現在不成立——真的種進去,助理每一輪都會去叫它,那一輪會停在
|
||
# 第一支自家腳本上,因為帶版本號的路徑進不了允許清單,而且是無聲的:沒有錯誤、沒有輸出,
|
||
# 心跳照寫,看起來完全正常。一個會無聲卡死的項目比一個缺掉的項目難查得多。
|
||
# 預設不種入的代價是「少了它,看不出為什麼」,那個代價用 held= 那幾行補掉:每一列印出技能名、
|
||
# 判定、條件原文與留在人手上的那一半,摘要另外印 held= 的筆數。少掉的那幾支看得見、也看得出
|
||
# 是刻意少的,不是漏的。
|
||
# 人確認過某一支的條件成立了,就帶 --allow-cond jsc-{domain}:{技能名} 種入那一支,一支一支帶,
|
||
# 不給一個「全部放行」的旗標:全部放行等於用一個決定蓋掉三個各自不同的條件。
|
||
#
|
||
# --- 清單改了 trigger、recur 或 way 怎麼辦 ---
|
||
#
|
||
# 規格的重建規則只有三條:清單新增可交項目就加一筆、清單移除或改成不交就移除那一筆、
|
||
# origin=user 的一律不動。一支技能還在清單上、還是可交,只是 trigger、recur 或 way 換了值,
|
||
# 三條規則一條都沒講到。這一支的處置是:**預設只印 drift= 報出來,不改那一筆。**
|
||
# 兩個理由。一、tasks.sh 刻意沒有 edit 操作,改欄位值只能移除再重新登錄,而重新登錄會換一個
|
||
# 新的 id,last_run 與 fail_count 從此歸零——一個原本已經連續失敗五次的項目會看起來像全新的,
|
||
# 沒有人會知道它壞了。二、規格沒講的事不自己補一條規則進去,尤其是會弄丟資料的那一種。
|
||
# 印出來,人看得到,要不要換由人決定。
|
||
# 人決定要換就帶 --refresh:那一筆先 remove 再 add,並且明說歷史會歸零。
|
||
#
|
||
# --- 空清單一律不動手 ---
|
||
#
|
||
# 清單讀不到(回 1)或讀到了卻解不出任何一列可交項目(回 2),這一支一筆都不移除,直接回報。
|
||
# 這一條是刻意寫的例外,不是漏掉:一致化的邏輯照字面跑,「清單上沒有」就等於「該移除」,
|
||
# 於是一個沒裝 jsc-meta 的機器、一個 clone 到一半的存取庫、一個被改壞的清單檔,都會讓這一支
|
||
# 把所有內建項一次刪光,而且回 0 看起來完全成功。刪光之後助理就再也不做任何定期檢查了,
|
||
# 而待辦簿上什麼都沒有,看不出曾經有過。
|
||
# 所以「清單是空的」與「清單說沒有可交項目」在這裡當成兩件事:前者是讀取失敗,一律不動手。
|
||
#
|
||
# --- 使用者交辦的那幾筆 ---
|
||
#
|
||
# origin=user 的待辦一律不動,這一支連讀都只讀不比對:不加、不移除、不算進 drift。
|
||
# 擋在兩層。這一支自己跳過那幾筆,而 tasks.sh 的 remove 對 origin=user 一律回 7,除非人親自
|
||
# 帶 --force——這一支一次都不帶那個旗標。手改過的檔案有可能出現 origin=user 卻帶著 spec_key
|
||
# 的組合(tasks.sh add 擋得住這個組合,手寫檔案擋不住),那幾筆印一行 skip_user= 並跳過,
|
||
# 不當成孤兒移除。
|
||
#
|
||
# 環境變數:
|
||
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc。要是連 HOME 也沒有就回 6,不猜
|
||
# JSC_DELEGATE_SPEC 委派清單的路徑,優先於 --root 與自動搜尋
|
||
# JSC_TASKS_SH 本 domain 的 tasks.sh 路徑,優先於自動搜尋
|
||
set -u
|
||
|
||
JSC_HOME_RAW="${JSC_HOME:-}"
|
||
if [ -z "$JSC_HOME_RAW" ]; then
|
||
JSC_HOME_RAW="${HOME:-}"
|
||
[ -n "$JSC_HOME_RAW" ] || {
|
||
printf '[jsc][助理內建項][ERR]:JSC_HOME 與 HOME 都沒有設定,找不到待辦簿的根目錄。這裡不猜一個路徑:猜錯就是拿另一個地方的待辦簿去對清單,於是把該有的那幾筆全部當成缺的再加一次。請設定 JSC_HOME 再跑一次。\n' >&2
|
||
exit 6
|
||
}
|
||
JSC_HOME_RAW="$JSC_HOME_RAW/.jsc"
|
||
fi
|
||
case "$JSC_HOME_RAW" in
|
||
/*) ;;
|
||
*)
|
||
_abs=$(CDPATH= cd -- "$JSC_HOME_RAW" 2>/dev/null && pwd -L) || _abs=''
|
||
[ -n "$_abs" ] || {
|
||
printf '[jsc][助理內建項][ERR]:JSC_HOME 是相對路徑(%s),也解不出絕對路徑。待辦簿的位置必須是字面絕對路徑,請把 JSC_HOME 設成絕對路徑再跑一次。\n' "$JSC_HOME_RAW" >&2
|
||
exit 6
|
||
}
|
||
JSC_HOME_RAW="$_abs" ;;
|
||
esac
|
||
|
||
JSC_HOME="$JSC_HOME_RAW"
|
||
CURRENT="$JSC_HOME/current"
|
||
|
||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
||
|
||
TAB=$(printf '\t')
|
||
|
||
die() { # $1=結束碼 $2=訊息
|
||
printf '[jsc][助理內建項][ERR]:%s\n' "$2" >&2
|
||
exit "$1"
|
||
}
|
||
warn() { printf '[jsc][助理內建項][WARN]:%s\n' "$1" >&2; }
|
||
note() { printf '[jsc][助理內建項]:%s\n' "$1" >&2; }
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
usage: seed-tasks.sh plan [--root 字面絕對路徑] [--spec 清單檔]
|
||
[--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||
seed-tasks.sh apply [--root 字面絕對路徑] [--spec 清單檔]
|
||
[--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||
EOF
|
||
exit 6
|
||
}
|
||
|
||
# 判準與處置同這個 domain 的其他腳本:只警告、照跑。從工作樹直接跑是開發時的正當用法。
|
||
warn_if_not_current() {
|
||
_want="$CURRENT/jsc-assist/tools/$(basename -- "$0")"
|
||
case "$SCRIPT_DIR/" in
|
||
"$CURRENT"/*) return 0 ;;
|
||
esac
|
||
warn "這支腳本是從 $SCRIPT_DIR/$(basename -- "$0") 跑起來的,不是 $_want。權限閘門只放行 current 那一組確切路徑:無人值守那一輪用別的路徑會被靜靜擋掉。開發時這樣跑沒關係。"
|
||
return 0
|
||
}
|
||
warn_if_not_current
|
||
|
||
valid_spec_key() {
|
||
printf '%s' "$1" | LC_ALL=C grep -qE '^jsc-[a-z0-9-]+:[a-z0-9-]+$'
|
||
}
|
||
|
||
# --- 找委派清單 ---
|
||
|
||
# 根目錄優先取 --root 給的字面絕對路徑,理由與技能本文取根目錄的規則相同:無人值守那一輪
|
||
# 自己不解根目錄,值由裝排程的人寫進條目、再一路餵下來。呼叫端沒給才自己找,找的順序比照
|
||
# patrol.sh 的 find_tool():先環境變數覆寫,再 current 那一組連結,然後開發用的並排存取庫
|
||
# 版面,最後已安裝的快取版面。
|
||
find_spec() {
|
||
if [ -n "${JSC_DELEGATE_SPEC:-}" ]; then
|
||
[ -f "$JSC_DELEGATE_SPEC" ] && { printf '%s\n' "$JSC_DELEGATE_SPEC"; return 0; }
|
||
return 1
|
||
fi
|
||
if [ -n "$OPT_SPEC" ]; then
|
||
[ -f "$OPT_SPEC" ] && { printf '%s\n' "$OPT_SPEC"; return 0; }
|
||
return 1
|
||
fi
|
||
if [ -n "$OPT_ROOT" ]; then
|
||
for _c in "$OPT_ROOT/jsc-meta/tools/delegate-spec.tsv" "$OPT_ROOT/meta/tools/delegate-spec.tsv"; do
|
||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
done
|
||
return 1
|
||
fi
|
||
for _c in "$CURRENT/jsc-meta/tools/delegate-spec.tsv" "$CURRENT/meta/tools/delegate-spec.tsv"; do
|
||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
done
|
||
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||
for _c in "$_root/../meta/tools/delegate-spec.tsv" "$_root/../jsc-meta/tools/delegate-spec.tsv"; do
|
||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
done
|
||
_c=$(ls -d "$_root"/../../jsc-meta/*/tools/delegate-spec.tsv \
|
||
"$_root"/../../meta/*/tools/delegate-spec.tsv \
|
||
"$HOME"/.claude/plugins/cache/*/jsc-meta/*/tools/delegate-spec.tsv 2>/dev/null \
|
||
| sort | tail -n1)
|
||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
return 1
|
||
}
|
||
|
||
# tasks.sh 一律取這一支腳本旁邊那一份,不去 current 或快取裡另外挑:兩支同 domain 的腳本
|
||
# 混到不同版本,欄位順序或結束碼一不一樣都看不出來,而寫入的是待辦簿本身。
|
||
find_tasks_sh() {
|
||
if [ -n "${JSC_TASKS_SH:-}" ]; then
|
||
[ -f "$JSC_TASKS_SH" ] && { printf '%s\n' "$JSC_TASKS_SH"; return 0; }
|
||
return 1
|
||
fi
|
||
[ -f "$SCRIPT_DIR/tasks.sh" ] && { printf '%s\n' "$SCRIPT_DIR/tasks.sh"; return 0; }
|
||
return 1
|
||
}
|
||
|
||
# --- way 與 probe 對 action ---
|
||
|
||
# way 是半形逗號隔開的清單,比對時前後各補一個逗號,才不會讓 invoke 去命中一個叫別的名字
|
||
# 但含有 invoke 這幾個字的交出方式。
|
||
is_invoke() { # $1=way
|
||
case ",$1," in
|
||
*,invoke,*) return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# 代入 {root} 用的字面絕對根目錄,整份清單只解一次,值放在 PROBE_ROOT。--root 給了就用那
|
||
# 一個,理由同 find_spec():根目錄由呼叫端餵進來,這一支不自己解也不猜。沒給才從清單自己的
|
||
# 位置往上推三層——清單一定在 {根目錄}/jsc-meta/tools/delegate-spec.tsv,推得回去;--spec
|
||
# 指到別處時推出來的值不一定對,所以每一列代完還要看那一層底下有沒有對應的 domain 目錄。
|
||
resolve_root() {
|
||
if [ -n "$OPT_ROOT" ]; then
|
||
PROBE_ROOT="$OPT_ROOT"
|
||
return 0
|
||
fi
|
||
PROBE_ROOT=$(CDPATH= cd -- "$(dirname -- "$SPEC")/../.." 2>/dev/null && pwd -L) || PROBE_ROOT=''
|
||
return 0
|
||
}
|
||
|
||
# 把 probe 那一行指令代好代入點。設好 PROBE_CMD 與 PROBE_HOLES 回 0,代不出來就設好
|
||
# PROBE_WHY 回 1。處置一律是退回 remind,理由見檔頭「probe 代不進去的時候一律退回 remind」。
|
||
# 這一支刻意用設全域變數的寫法、不用命令替換接回傳值:命令替換是子行程,裡面設的
|
||
# PROBE_HOLES 與 PROBE_WHY 傳不回來,於是每一筆的 holes= 與失敗理由都會是空的。
|
||
probe_action() { # $1=probe 原文
|
||
PROBE_CMD=''
|
||
PROBE_HOLES=''
|
||
PROBE_WHY=''
|
||
# 金錢符號與波浪號先擋。那兩種寫法在無人值守那一輪解不出來,也進不了允許清單,會被靜靜
|
||
# 擋掉;擋在種入這一刻,錯的是清單這件事才看得見。
|
||
case "$1" in
|
||
*'$'*) PROBE_WHY='指令裡有金錢符號。那種寫法在無人值守那一輪解不出來,也進不了允許清單,會被靜靜擋掉'; return 1 ;;
|
||
*'~'*) PROBE_WHY='指令裡有波浪號。那種寫法在無人值守那一輪解不出來,也進不了允許清單,會被靜靜擋掉'; return 1 ;;
|
||
esac
|
||
# 腳本路徑一律寫成 {root}/jsc-{domain}/ 開頭。認不出這個形狀就代不出絕對路徑,而權限閘門
|
||
# 只放行完整字面絕對路徑。
|
||
_dom=$(printf '%s' "$1" | LC_ALL=C sed -n 's|.*{root}/jsc-\([a-z0-9-]*\)/.*|\1|p')
|
||
if [ -z "$_dom" ]; then
|
||
PROBE_WHY='路徑不是 {root}/jsc-{domain}/ 開頭,代不出字面絕對路徑'
|
||
return 1
|
||
fi
|
||
if [ -z "$PROBE_ROOT" ]; then
|
||
PROBE_WHY="代不出根目錄(清單在 $SPEC,也沒有帶 --root)"
|
||
return 1
|
||
fi
|
||
# 那一層的目錄名以 jsc-{domain} 為準,找不到就退回不帶前綴的 {domain},比照 find_spec()
|
||
# 找清單本身的作法:開發用的並排存取庫版面那一層就是不帶前綴的。
|
||
if [ -d "$PROBE_ROOT/jsc-$_dom" ]; then
|
||
_dir="jsc-$_dom"
|
||
elif [ -d "$PROBE_ROOT/$_dom" ]; then
|
||
_dir="$_dom"
|
||
else
|
||
PROBE_WHY="這台機器的 $PROBE_ROOT 底下找不到 jsc-$_dom,也找不到 $_dom,那一段唯讀盤點的腳本不在這裡"
|
||
return 1
|
||
fi
|
||
_pre="${1%%\{root\}/jsc-$_dom/*}"
|
||
_post="${1#*\{root\}/jsc-$_dom/}"
|
||
_cmd="$_pre$PROBE_ROOT/$_dir/$_post"
|
||
# 指到不存在的腳本算缺失。種進去的話那一筆每一輪失敗一次,而失敗的原因在清單那一邊。
|
||
_script="$PROBE_ROOT/$_dir/${_post%% *}"
|
||
if [ ! -f "$_script" ]; then
|
||
PROBE_WHY="代出來的腳本不存在:$_script"
|
||
return 1
|
||
fi
|
||
# {root} 代完之後還准留的大括號只有 {cli} 與 {repo} 兩種。其餘一律算填錯:代不進去的字面值
|
||
# 會原樣送進指令。
|
||
_left=$(printf '%s' "$_cmd" | sed 's/{cli}//g; s/{repo}//g')
|
||
case "$_left" in
|
||
*'{'*|*'}'*)
|
||
PROBE_WHY="代完 {root} 之後還留著認不得的代入點:$_cmd"
|
||
return 1 ;;
|
||
esac
|
||
case "$_cmd" in
|
||
*'{cli}'*) PROBE_HOLES='{cli}' ;;
|
||
esac
|
||
case "$_cmd" in
|
||
*'{repo}'*) PROBE_HOLES="${PROBE_HOLES:+$PROBE_HOLES,}{repo}" ;;
|
||
esac
|
||
PROBE_CMD="$_cmd"
|
||
return 0
|
||
}
|
||
|
||
# --- 參數 ---
|
||
|
||
MODE="${1:-}"
|
||
[ -n "$MODE" ] || usage
|
||
case "$MODE" in
|
||
plan|apply) ;;
|
||
*) usage ;;
|
||
esac
|
||
shift
|
||
|
||
OPT_ROOT=''
|
||
OPT_SPEC=''
|
||
OPT_REFRESH=0
|
||
ALLOW_COND=''
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--root) [ "$#" -ge 2 ] || usage; OPT_ROOT="$2"; shift 2 ;;
|
||
--spec) [ "$#" -ge 2 ] || usage; OPT_SPEC="$2"; shift 2 ;;
|
||
--refresh) OPT_REFRESH=1; shift ;;
|
||
--allow-cond)
|
||
[ "$#" -ge 2 ] || usage
|
||
valid_spec_key "$2" || die 6 "--allow-cond「$2」不是 jsc-{domain}:{技能名} 這個形狀。形狀不對的值放行不了任何一支,而那一支會被當成沒放行、靜靜少掉。"
|
||
ALLOW_COND="$ALLOW_COND $2"
|
||
shift 2 ;;
|
||
*) usage ;;
|
||
esac
|
||
done
|
||
|
||
case "$OPT_ROOT" in
|
||
''|/*) ;;
|
||
*) die 6 "--root 給的是「$OPT_ROOT」,不是絕對路徑。根目錄由呼叫端餵進來,這一支不自己解、也不猜:相對路徑在排程那一輪等於指向排程機制的工作目錄。" ;;
|
||
esac
|
||
|
||
allowed_cond() { # $1=spec_key
|
||
for _a in $ALLOW_COND; do
|
||
[ "$_a" = "$1" ] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# --- 前置 ---
|
||
|
||
SPEC=$(find_spec) || SPEC=''
|
||
[ -n "$SPEC" ] || die 1 "找不到委派清單(jsc-meta 的 tools/delegate-spec.tsv)。這一支一筆都不動:讀不到清單的時候,「清單上沒有」與「這台機器沒裝 jsc-meta」分不出來,照字面跑會把所有內建項一次刪光。請安裝 jsc-meta,或用 --root 指定工具根目錄、用 --spec 直接指定清單檔。"
|
||
[ -r "$SPEC" ] || die 1 "委派清單在 $SPEC,可是讀不到。一筆都不動,理由同上。請檢查那個檔案的權限。"
|
||
|
||
TASKS_SH=$(find_tasks_sh) || TASKS_SH=''
|
||
[ -n "$TASKS_SH" ] || die 3 "找不到本 domain 的 tasks.sh(找過 $SCRIPT_DIR)。待辦簿只有那一支寫得出來,這一支不自己動 tasks/ 底下的檔案,所以這一次一筆都不動。"
|
||
|
||
TMPD=$(mktemp -d 2>/dev/null) || die 5 '建不出暫存目錄,這一輪算不出要改哪幾筆。'
|
||
trap 'rm -rf "$TMPD"' EXIT
|
||
WANT="$TMPD/want.tsv"
|
||
HAVE="$TMPD/have.tsv"
|
||
: >"$WANT" 2>/dev/null || die 5 "暫存檔寫不進去:$WANT。"
|
||
: >"$HAVE" 2>/dev/null || die 5 "暫存檔寫不進去:$HAVE。"
|
||
|
||
# --- 讀清單,算出「該有哪幾筆」 ---
|
||
|
||
N_HELD=0
|
||
N_BAD=0
|
||
N_PROBE=0
|
||
N_PENDING=0
|
||
N_PROBE_BAD=0
|
||
PROBE_ROOT=''
|
||
PROBE_CMD=''
|
||
PROBE_HOLES=''
|
||
PROBE_WHY=''
|
||
resolve_root
|
||
|
||
# 這一份清單有沒有 probe 那一欄,先數欄位數判一次,理由與判法見檔頭「清單只有十一欄的
|
||
# 時候」。註解列不算:檔頭那幾行的欄位數是說明文字切出來的,跟資料列無關。
|
||
SPEC_COLS=$(awk -F"$TAB" '/^#/{next} NF>1 {if (NF>m) m=NF} END{print m+0}' "$SPEC" 2>/dev/null)
|
||
case "$SPEC_COLS" in
|
||
''|*[!0-9]*) SPEC_COLS=0 ;;
|
||
esac
|
||
if [ "$SPEC_COLS" -lt 12 ]; then
|
||
note "委派清單 $SPEC 只有 $SPEC_COLS 欄,沒有第十二欄 probe。這一輪所有非 invoke 的列照舊一律種成只提醒,行為與清單補上那一欄之前相同。要讓有唯讀盤點入口的那幾列真的動手,請先把 jsc-meta 更新到有 probe 那一欄的版本,再跑一次 apply——那一次那幾筆會印成 drift=,要換值得帶 --refresh。"
|
||
fi
|
||
|
||
# 註解列與空白列跳掉。清單是定位字元分隔,欄位順序見清單自己的檔頭。
|
||
# c_rest 收第十三欄以後:少了它,上游哪天加一欄,多出來的值會連著定位字元黏進 c_probe,
|
||
# 而黏出來的那一行看起來還很像一個完整的指令。
|
||
while IFS="$TAB" read -r c_domain c_name c_verdict c_way c_slice c_human c_trigger c_recur \
|
||
c_next c_version c_origin c_probe c_rest; do
|
||
case "$c_domain" in
|
||
''|'#'*) continue ;;
|
||
esac
|
||
[ -n "$c_name" ] || continue
|
||
_key="jsc-$c_domain:$c_name"
|
||
case "$c_verdict" in
|
||
none)
|
||
# 不交的那幾列本來就不該在待辦簿裡,連 held 都不算:它們是判定過決定不交,不是條件
|
||
# 還沒成立。清單上沒有它們,重建時對應那一筆就會被移除,那正是規格要的行為。
|
||
continue ;;
|
||
full|slice) ;;
|
||
cond)
|
||
if allowed_cond "$_key"; then
|
||
note "$_key 是條件式交,這一次由 --allow-cond 放行,照 full 與 slice 同一套種入。條件成立與否由帶這個旗標的人負責。"
|
||
else
|
||
# 放行過的那幾支不算保留:摘要的 held= 要等於「這一次少掉幾支」,把放行的也算進去,
|
||
# 那個數字就跟上面的 held= 行數對不起來。
|
||
N_HELD=$((N_HELD + 1))
|
||
printf 'held=%s verdict=cond way=%s\n' "$_key" "$c_way"
|
||
printf ' 條件:%s\n' "$c_slice"
|
||
printf ' 留在人手上:%s\n' "$c_human"
|
||
continue
|
||
fi ;;
|
||
*)
|
||
N_BAD=$((N_BAD + 1))
|
||
printf 'bad=%s reason=判定欄的值是「%s」,不在 full、slice、cond、none 四個裡面\n' "$_key" "$c_verdict"
|
||
continue ;;
|
||
esac
|
||
# 清單說可交,trigger 或 recur 卻沒有值,這一筆種不出來:補一個猜出來的時程等於讓助理
|
||
# 拿一個沒有人同意過的排程去跑。印出來讓人回去補清單。
|
||
if [ -z "$c_trigger" ] || [ "$c_trigger" = '-' ] || [ -z "$c_recur" ] || [ "$c_recur" = '-' ]; then
|
||
N_BAD=$((N_BAD + 1))
|
||
printf 'bad=%s reason=判定是 %s 卻沒有 trigger 或 recur(trigger=%s recur=%s),種不出來\n' \
|
||
"$_key" "$c_verdict" "${c_trigger:--}" "${c_recur:--}"
|
||
continue
|
||
fi
|
||
# way 決定 action 的大方向,probe 只在「不是 invoke」那一路上改動它。對映見檔頭
|
||
# 「way 與 probe 怎麼推出 action」。
|
||
_probe="$c_probe"
|
||
# 十一欄的舊清單一律當成沒有 probe,不逐列印警告:上面已經整份講過一次。
|
||
[ "$SPEC_COLS" -ge 12 ] || _probe=''
|
||
if is_invoke "$c_way"; then
|
||
_action="$_key"
|
||
case "$_probe" in
|
||
''|'-') ;;
|
||
*)
|
||
N_PROBE_BAD=$((N_PROBE_BAD + 1))
|
||
printf 'probe_bad=%s reason=way 含 invoke,這一次照 way 取技能名,probe 沒有採用 probe=%s\n' \
|
||
"$_key" "$_probe" ;;
|
||
esac
|
||
else
|
||
_action=remind
|
||
case "$_probe" in
|
||
''|'-') ;;
|
||
'pending:')
|
||
# 冒號後面留白的話,下一輪分不出是刻意不接還是漏填,所以當成填錯報出來,動作照樣
|
||
# 退回只提醒。
|
||
N_PROBE_BAD=$((N_PROBE_BAD + 1))
|
||
printf 'probe_bad=%s reason=probe 寫了 pending 卻沒有寫理由,分不出是刻意不接還是漏填\n' "$_key" ;;
|
||
pending:*)
|
||
N_PENDING=$((N_PENDING + 1))
|
||
printf 'pending=%s reason=%s\n' "$_key" "${_probe#pending:}" ;;
|
||
*)
|
||
if probe_action "$_probe"; then
|
||
_action="$PROBE_CMD"
|
||
N_PROBE=$((N_PROBE + 1))
|
||
printf 'probe=%s holes=%s action=%s\n' "$_key" "${PROBE_HOLES:--}" "$PROBE_CMD"
|
||
else
|
||
N_PROBE_BAD=$((N_PROBE_BAD + 1))
|
||
printf 'probe_bad=%s reason=%s probe=%s\n' "$_key" "$PROBE_WHY" "$_probe"
|
||
fi ;;
|
||
esac
|
||
fi
|
||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||
"$_key" 'check' "委派清單內建項:$_key" "$_action" \
|
||
"$c_trigger" "$c_recur" >>"$WANT" 2>/dev/null \
|
||
|| die 5 "暫存檔寫不進去:$WANT。"
|
||
done <"$SPEC"
|
||
|
||
N_WANT=$(awk 'END{print NR+0}' "$WANT")
|
||
if [ "$N_WANT" -eq 0 ]; then
|
||
printf 'mode=%s spec=%s want=0 held=%s bad=%s\n' "$MODE" "$SPEC" "$N_HELD" "$N_BAD"
|
||
die 2 "委派清單 $SPEC 讀到了,卻一列可交項目都解不出來(保留 $N_HELD 列、對不上 $N_BAD 列)。一筆都不移除:一份被改壞或抄到一半的清單照字面跑會把所有內建項刪光,而那之後助理就不再做任何定期檢查,待辦簿上也看不出曾經有過。請先確認清單本身。"
|
||
fi
|
||
|
||
# --- 讀待辦簿,算出「現在有哪幾筆」 ---
|
||
|
||
# list 的欄位順序:id、kind、state、title、action、trigger、recur、repo、due、last_run、
|
||
# next_run、fail_count、origin、spec_key。這裡只留帶 spec_key 的那幾筆,其餘與清單無關。
|
||
if ! "$TASKS_SH" list --no-header >"$TMPD/list.tsv" 2>"$TMPD/list.err"; then
|
||
cat "$TMPD/list.err" >&2
|
||
die 3 "$TASKS_SH list 回了非 0,讀不出待辦簿現況。一筆都不動:讀不到現況的時候,每一筆都會被當成缺的再加一次。"
|
||
fi
|
||
awk -F"$TAB" 'NF>=14 && $14!=""{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",$14,$1,$13,$2,$4,$5,$6,$7}' \
|
||
"$TMPD/list.tsv" >"$HAVE" 2>/dev/null || die 5 "暫存檔寫不進去:$HAVE。"
|
||
|
||
N_SKIP_USER=0
|
||
N_DUP=0
|
||
|
||
# --- 一致化 ---
|
||
|
||
N_ADD=0
|
||
N_REMOVE=0
|
||
N_KEEP=0
|
||
N_DRIFT=0
|
||
RC_PARTIAL=0
|
||
|
||
run_add() { # $1=spec_key $2=kind $3=title $4=action $5=trigger $6=recur
|
||
if [ "$MODE" = plan ]; then
|
||
printf 'add=%s action=%s trigger=%s recur=%s title=%s(plan,沒有寫進去)\n' "$1" "$4" "$5" "$6" "$3"
|
||
N_ADD=$((N_ADD + 1))
|
||
return 0
|
||
fi
|
||
# 結束碼要在呼叫的下一行就接住。寫成 if 的條件再到後面讀 $?,讀到的是那個 if 整段的碼,
|
||
# 失敗那一路永遠是 0,於是每一筆失敗都會被回報成 exit=0。
|
||
"$TASKS_SH" add --kind "$2" --title "$3" --action "$4" --trigger "$5" \
|
||
--recur "$6" --origin assistant --spec-key "$1" >"$TMPD/add.out" 2>"$TMPD/add.err"
|
||
_rc=$?
|
||
if [ "$_rc" -eq 0 ]; then
|
||
_id=$(sed -n 's/^added=\([0-9A-Fa-f]*\).*/\1/p' "$TMPD/add.out" | head -n1)
|
||
printf 'add=%s id=%s action=%s trigger=%s recur=%s\n' "$1" "${_id:--}" "$4" "$5" "$6"
|
||
N_ADD=$((N_ADD + 1))
|
||
return 0
|
||
fi
|
||
cat "$TMPD/add.err" >&2
|
||
printf 'add_failed=%s exit=%s\n' "$1" "$_rc"
|
||
RC_PARTIAL=1
|
||
return 1
|
||
}
|
||
|
||
run_remove() { # $1=spec_key $2=id $3=理由
|
||
if [ "$MODE" = plan ]; then
|
||
printf 'remove=%s id=%s reason=%s(plan,沒有刪掉)\n' "$1" "$2" "$3"
|
||
N_REMOVE=$((N_REMOVE + 1))
|
||
return 0
|
||
fi
|
||
# 一次都不帶 --force:那個旗標留給人。無人值守那一輪碰到 origin=user 就該被擋下。
|
||
"$TASKS_SH" remove "$2" >"$TMPD/rm.out" 2>"$TMPD/rm.err"
|
||
_rc=$?
|
||
if [ "$_rc" -eq 0 ]; then
|
||
printf 'remove=%s id=%s reason=%s\n' "$1" "$2" "$3"
|
||
N_REMOVE=$((N_REMOVE + 1))
|
||
return 0
|
||
fi
|
||
cat "$TMPD/rm.err" >&2
|
||
printf 'remove_failed=%s id=%s exit=%s\n' "$1" "$2" "$_rc"
|
||
RC_PARTIAL=1
|
||
return 1
|
||
}
|
||
|
||
# 一、清單上有的,待辦簿裡有沒有。
|
||
while IFS="$TAB" read -r w_key w_kind w_title w_action w_trigger w_recur; do
|
||
_hits=$(awk -F"$TAB" -v k="$w_key" '$1==k{n++} END{print n+0}' "$HAVE")
|
||
if [ "$_hits" -eq 0 ]; then
|
||
run_add "$w_key" "$w_kind" "$w_title" "$w_action" "$w_trigger" "$w_recur" || true
|
||
continue
|
||
fi
|
||
if [ "$_hits" -gt 1 ]; then
|
||
# 同一個鍵有兩筆以上,這一支不猜該留哪一筆:兩筆的 last_run 與 fail_count 不同,刪錯
|
||
# 的那一筆的歷史就沒了。印出來讓人挑。
|
||
N_DUP=$((N_DUP + 1))
|
||
printf 'dup=%s count=%s reason=同一個清單鍵有多筆,這一支不動它,請人留一筆\n' "$w_key" "$_hits"
|
||
awk -F"$TAB" -v k="$w_key" '$1==k{printf " dup_id=%s origin=%s title=%s\n",$2,$3,$5}' "$HAVE"
|
||
continue
|
||
fi
|
||
_id=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $2}' "$HAVE")
|
||
_origin=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $3}' "$HAVE")
|
||
if [ "$_origin" != assistant ]; then
|
||
# 手改過的檔案才會出現這個組合,tasks.sh add 擋得住。一律不動,也不算孤兒。
|
||
N_SKIP_USER=$((N_SKIP_USER + 1))
|
||
printf 'skip_user=%s id=%s origin=%s reason=帶清單鍵但不是助理內建,一律不動\n' "$w_key" "$_id" "$_origin"
|
||
continue
|
||
fi
|
||
_h_kind=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $4}' "$HAVE")
|
||
_h_action=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $6}' "$HAVE")
|
||
_h_trigger=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $7}' "$HAVE")
|
||
_h_recur=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $8}' "$HAVE")
|
||
_diff=''
|
||
[ "$_h_kind" = "$w_kind" ] || _diff="$_diff kind:$_h_kind → $w_kind"
|
||
[ "$_h_action" = "$w_action" ] || _diff="$_diff action:$_h_action → $w_action"
|
||
[ "$_h_trigger" = "$w_trigger" ] || _diff="$_diff trigger:$_h_trigger → $w_trigger"
|
||
[ "$_h_recur" = "$w_recur" ] || _diff="$_diff recur:$_h_recur → $w_recur"
|
||
if [ -z "$_diff" ]; then
|
||
printf 'keep=%s id=%s\n' "$w_key" "$_id"
|
||
N_KEEP=$((N_KEEP + 1))
|
||
continue
|
||
fi
|
||
N_DRIFT=$((N_DRIFT + 1))
|
||
if [ "$OPT_REFRESH" -eq 1 ]; then
|
||
printf 'drift=%s id=%s refresh=1 diff=%s\n' "$w_key" "$_id" "$_diff"
|
||
if run_remove "$w_key" "$_id" '清單的判定結果換了值,--refresh 要求重新登錄'; then
|
||
run_add "$w_key" "$w_kind" "$w_title" "$w_action" "$w_trigger" "$w_recur" || true
|
||
fi
|
||
else
|
||
printf 'drift=%s id=%s refresh=0 diff=%s\n' "$w_key" "$_id" "$_diff"
|
||
printf ' 這一筆沒有改。要照清單換值請帶 --refresh,那一筆會先移除再重新登錄,id 會換,last_run 與 fail_count 會歸零。\n'
|
||
N_KEEP=$((N_KEEP + 1))
|
||
fi
|
||
done <"$WANT"
|
||
|
||
# 二、待辦簿裡有、清單上沒有的,就是孤兒。
|
||
while IFS="$TAB" read -r h_key h_id h_origin h_kind h_title h_action h_trigger h_recur; do
|
||
[ -n "$h_key" ] || continue
|
||
if awk -F"$TAB" -v k="$h_key" '$1==k{f=1} END{exit f?0:1}' "$WANT"; then
|
||
continue
|
||
fi
|
||
if [ "$h_origin" != assistant ]; then
|
||
N_SKIP_USER=$((N_SKIP_USER + 1))
|
||
printf 'skip_user=%s id=%s origin=%s reason=帶清單鍵但不是助理內建,一律不動\n' "$h_key" "$h_id" "$h_origin"
|
||
continue
|
||
fi
|
||
run_remove "$h_key" "$h_id" '清單上已經沒有這一支,或它改判成不交' || true
|
||
done <"$HAVE"
|
||
|
||
# --- 摘要 ---
|
||
|
||
N_HAVE=$(awk 'END{print NR+0}' "$HAVE")
|
||
printf 'mode=%s spec=%s spec_cols=%s root=%s tasks_sh=%s want=%s have=%s added=%s removed=%s kept=%s drift=%s held=%s bad=%s dup=%s skip_user=%s probe=%s pending=%s probe_bad=%s\n' \
|
||
"$MODE" "$SPEC" "$SPEC_COLS" "${PROBE_ROOT:--}" "$TASKS_SH" "$N_WANT" "$N_HAVE" "$N_ADD" "$N_REMOVE" "$N_KEEP" \
|
||
"$N_DRIFT" "$N_HELD" "$N_BAD" "$N_DUP" "$N_SKIP_USER" "$N_PROBE" "$N_PENDING" "$N_PROBE_BAD"
|
||
|
||
[ "$N_HELD" -gt 0 ] && note "有 $N_HELD 支是條件式交,這一次沒有種入,逐支印在上面的 held= 那幾行。條件是散文,程式判不了;人確認過某一支的條件成立就帶 --allow-cond 那一支的鍵。"
|
||
[ "$N_BAD" -gt 0 ] && warn "清單上有 $N_BAD 列對不上,那幾支這一次沒有種入,逐列印在上面的 bad= 那幾行。請回去補清單,不要在這裡補預設值。"
|
||
[ "$N_PENDING" -gt 0 ] && note "有 $N_PENDING 支切得出唯讀盤點、入口還沒接上,逐支印在上面的 pending= 那幾行,理由是清單上的原文。那幾筆的動作是只提醒,跟本來就只提醒的那幾筆分別在這裡,待辦檔上看不出來。"
|
||
[ "$N_PROBE_BAD" -gt 0 ] && warn "有 $N_PROBE_BAD 支的 probe 代不進去,逐支印在上面的 probe_bad= 那幾行。那幾筆照樣種入,動作退回只提醒——不種入等於讓清單上一格填錯把一筆帶著歷史的內建項刪掉。請回去補清單,或把缺的 plugin 裝起來。"
|
||
[ "$N_PROBE" -gt 0 ] && note "有 $N_PROBE 支的動作是一行唯讀盤點指令,逐支印在上面的 probe= 那幾行。holes= 不是減號的那幾筆還留著代入點,執行那一步要先把 {cli} 換成偵測到的 CLI 代號、把 {repo} 換成掃到的存取庫工作目錄,一個目標跑一次;帶著大括號的指令一律不得原樣執行。"
|
||
[ "$N_DRIFT" -gt 0 ] && [ "$OPT_REFRESH" -eq 0 ] && note "有 $N_DRIFT 筆的判定結果與清單不一樣,這一次照原樣留著。要換值請帶 --refresh,並且知道那一筆的 last_run 與 fail_count 會歸零。"
|
||
|
||
[ "$RC_PARTIAL" -eq 0 ] || die 4 "有 add 或 remove 失敗,其餘各筆照做完了。失敗的逐筆印在上面的 add_failed= 與 remove_failed= 那幾行,各自帶了 tasks.sh 的結束碼,照那一支的結束碼表處理。"
|
||
exit 0
|