feat(seed): 種入內建項時改讀委派清單的唯讀盤點指令欄

委派清單補上第十二欄 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>
This commit is contained in:
2026-09-04 10:16:13 +08:00
co-authored by Claude Opus 5
parent 1f02c5843d
commit 28f453176e
6 changed files with 265 additions and 36 deletions
+234 -24
View File
@@ -41,13 +41,13 @@
#
# --- 哪些欄位從清單來、哪些自己補 ---
#
# 清單有十一欄,待辦簿有十五個鍵,對得上的只有兩欄。逐個交代:
# 清單有十二欄,待辦簿有十五個鍵,直接抄過來的只有兩欄。逐個交代:
# 從清單直接抄過來
# trigger 原樣抄。第一次什麼時候到期是判定結果,不是這一支的決定
# recur 原樣抄。同上
# 從清單推出來
# spec_key domain 與 name 兩欄合起來寫成 jsc-{domain}:{技能名},那就是一支技能的身分
# action 由 way 推,對映見下一段。way 與 action 不是同一件事
# action 由 way 與 probe 兩欄一起推,對映見下一段。way 與 action 不是同一件事
# title 固定寫成「委派清單內建項:{spec_key}」。標題刻意不含 way、trigger、recur:
# 那幾欄會隨清單改動而變,寫進標題就等於每一次改動都換一個 id,而 id 一換,
# last_run 與 fail_count 的歷史就跟著斷掉
@@ -68,19 +68,92 @@
# 兩個同名不同義,一律不互抄
# state、last_run、next_run、fail_count 由 tasks.sh 與判到期那一邊維護,這一支不碰。
#
# --- way 與 action 的對映 ---
# --- way 與 probe 怎麼推出 action ---
#
# 清單的 way 是交出方式,三種:invoke 觸發、patrol 巡檢、remind 提醒。待辦簿的 action 是
# 助理實際要跑的事,三種:技能名、腳本、remind。兩套詞彙不對應,所以要明寫對映:
# way 含 invoke → action 取技能名。觸發的定義就是呼叫既有技能、內容照那支技能自己的
# 流程走,所以填技能名就是照判定結果做
# 其餘(patrol、remind、patrol,remind)→ action 取 remind
# 第二條的理由要講清楚,因為它看起來像偷懶。巡檢那個交出方式的意思是「助理在自己那一輪裡
# 順手做那一段唯讀盤點」,而那一段沒有獨立的入口:巡檢那一輪讀的是固定那幾項來源,沒有
# 一支腳本或一個技能名代表得了「某一支技能的唯讀切片」。這時候把技能名填進 action,助理下一輪
# 就會去呼叫整支技能,那正是切片交要防的事——留在人手上的那一半會被一路跑完。
# 所以填 remind:助理照時程提醒該做那一段、指出入口,不動手。等哪一天那些切片各自有了自己的
# 入口,改的是這個對映,不是待辦簿的格式。
# 助理實際要跑的事,三種:技能名、指令、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 讀到的就是「指令加一個定位字元加第十三欄」,代入點檢查
# 全部過得了關,最後拿去執行的是一行誰都沒寫過的指令。
#
# --- 條件式交的那幾支一律不種入 ---
#
@@ -238,15 +311,90 @@ find_tasks_sh() {
return 1
}
# --- way 對 action ---
# --- way 與 probe 對 action ---
# 對映與理由見檔頭「way 與 action 的對映」。way 是半形逗號隔開的清單,比對時前後各補一個
# 逗號,才不會讓 invoke 去命中一個叫別的名字但含有 invoke 這幾個字的交出方式。
action_of() { # $1=way $2=spec_key
# way 是半形逗號隔開的清單,比對時前後各補一個逗號,才不會讓 invoke 去命中一個叫別的名字
# 但含有 invoke 這幾個字的交出方式。
is_invoke() { # $1=way
case ",$1," in
*,invoke,*) printf '%s' "$2" ;;
*) printf 'remind' ;;
*,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
}
# --- 參數 ---
@@ -309,8 +457,30 @@ HAVE="$TMPD/have.tsv"
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
# 註解列與空白列跳掉。清單是定位字元分隔,欄位順序見清單自己的檔頭。
while IFS="$TAB" read -r c_domain c_name c_verdict c_way c_slice c_human c_trigger c_recur c_rest; do
# 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
@@ -347,8 +517,45 @@ while IFS="$TAB" read -r c_domain c_name c_verdict c_way c_slice c_human c_trigg
"$_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_of "$c_way" "$_key")" \
"$_key" 'check' "委派清單內建項:$_key" "$_action" \
"$c_trigger" "$c_recur" >>"$WANT" 2>/dev/null \
|| die 5 "暫存檔寫不進去:$WANT。"
done <"$SPEC"
@@ -491,12 +698,15 @@ done <"$HAVE"
# --- 摘要 ---
N_HAVE=$(awk 'END{print NR+0}' "$HAVE")
printf 'mode=%s spec=%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\n' \
"$MODE" "$SPEC" "$TASKS_SH" "$N_WANT" "$N_HAVE" "$N_ADD" "$N_REMOVE" "$N_KEEP" \
"$N_DRIFT" "$N_HELD" "$N_BAD" "$N_DUP" "$N_SKIP_USER"
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 的結束碼,照那一支的結束碼表處理。"