Merge pull request '釋出:種入內建項時改讀委派清單的唯讀盤點指令欄' (#21) from develop into master

Reviewed-on: #21
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #21.
This commit is contained in:
2026-09-04 03:37:48 +00:00
6 changed files with 265 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.1.9", "version": "0.2.0",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills", "skills": "./skills",
"author": { "author": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.1.9", "version": "0.2.0",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills", "skills": "./skills",
"jsc": { "jsc": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.1.9", "version": "0.2.0",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills/", "skills": "./skills/",
"jsc": { "jsc": {
File diff suppressed because one or more lines are too long
+24 -5
View File
@@ -132,7 +132,7 @@ The call never changes the outcome: it returns 0 even when it cannot write, and
| `$JSC_HOME/assistant/heartbeat` | `heartbeat.sh` only, never this skill | `key=value` lines: `ts`, `pid`, `cli`, `session` | | `$JSC_HOME/assistant/heartbeat` | `heartbeat.sh` only, never this skill | `key=value` lines: `ts`, `pid`, `cli`, `session` |
| `$JSC_HOME/assistant/schedule.log` | nobody here — the scheduled entry appends to it | free text; point the operator at it when a scheduled round misbehaves | | `$JSC_HOME/assistant/schedule.log` | nobody here — the scheduled entry appends to it | free text; point the operator at it when a scheduled round misbehaves |
| `$JSC_HOME/assistant/tasks/{id}` | this skill reads it directly; every write goes through `tasks.sh` | `key=value` lines, one task per file: `id`, `created`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`), `spec_key` (`jsc-{domain}:{skill}` for a built-in item, empty for anything a person asked for) | | `$JSC_HOME/assistant/tasks/{id}` | this skill reads it directly; every write goes through `tasks.sh` | `key=value` lines, one task per file: `id`, `created`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`), `spec_key` (`jsc-{domain}:{skill}` for a built-in item, empty for anything a person asked for) |
| `{CURRENT}/jsc-meta/tools/delegate-spec.tsv` | `seed-tasks.sh` only, read-only | the delegation list, tab-separated, one skill per row. `verdict`, `way`, `trigger` and `recur` are what decide whether a skill gets a built-in check item and on what schedule | | `{CURRENT}/jsc-meta/tools/delegate-spec.tsv` | `seed-tasks.sh` only, read-only | the delegation list, tab-separated, one skill per row. `verdict`, `way`, `trigger` and `recur` decide whether a skill gets a built-in check item and on what schedule; column 12, `probe`, decides what that item's `action` actually is — a one-line read-only command, `pending:{reason}` for a slice whose entry point is not wired yet, or `-` for a row that has none. A list with only 11 columns predates that column and is handled as if every row said `-` |
| `$JSC_HOME/assistant/patrol.lock/` | `patrol.sh` only | the round lock, a directory. `info` holds `round`, `pid`, `started` | | `$JSC_HOME/assistant/patrol.lock/` | `patrol.sh` only | the round lock, a directory. `info` holds `round`, `pid`, `started` |
| `$JSC_HOME/assistant/patrol/` | `patrol.sh` only | one round's scratch files, including `latest.md`, `summary.md`, `summary-row.md`, `newpage.md` and `contents-entry.md` | | `$JSC_HOME/assistant/patrol/` | `patrol.sh` only | one round's scratch files, including `latest.md`, `summary.md`, `summary-row.md`, `newpage.md` and `contents-entry.md` |
| `$JSC_HOME/assistant/usage-prev.tsv` | `patrol.sh` only | last recorded round's cumulative usage counts, so the next round can print a real per-round delta | | `$JSC_HOME/assistant/usage-prev.tsv` | `patrol.sh` only | last recorded round's cumulative usage counts, so the next round can print a real per-round delta |
@@ -243,6 +243,25 @@ The delegation list holds one row per jsc skill and records whether that skill c
| The row is still delegable but its `trigger`, `recur` or `way` changed | report it as `drift=` and change nothing, unless `--refresh` was passed | | The row is still delegable but its `trigger`, `recur` or `way` changed | report it as `drift=` and change nothing, unless `--refresh` was passed |
| The row's `verdict` is `cond` | hold it: seed nothing, print a `held=` line carrying the condition text, unless `--allow-cond {key}` named that row | | The row's `verdict` is `cond` | hold it: seed nothing, print a `held=` line carrying the condition text, unless `--allow-cond {key}` named that row |
## What a built-in item actually does, and the `probe` column
`way` decides the shape of the entry's `action`, and column 12 `probe` decides the rest of it:
| `way` | `probe` | `action` | Also printed |
| --- | --- | --- | --- |
| contains `invoke` | ignored entirely | the skill name | a `probe_bad=` line if `probe` held a command — the list's own header says an `invoke` row carries `-`, and honouring a command there would turn a whole delegated skill into one script call that writes none of the pages that skill exists to write, while looking perfectly healthy |
| `patrol`, `remind`, `patrol,remind` | a one-line command | that command, substituted | a `probe=` line with the command and its remaining holes |
| `patrol`, `remind`, `patrol,remind` | `pending:{reason}` | `remind` | a `pending=` line carrying the reason verbatim |
| `patrol`, `remind`, `patrol,remind` | `-`, empty, or the whole list is 11 columns | `remind` | nothing — this is the behaviour that predates the column |
**A `probe` that cannot be substituted falls back to `remind` and is reported, never dropped.** The path is not `{root}/jsc-{domain}/...`, the command carries a `$` or a `~`, a brace other than the three known holes survived, the root could not be resolved, or the script is not on this machine: each prints `probe_bad=` and the entry is still seeded, as a reminder. Not seeding it would mean removing it on `apply`, so one mistyped cell upstream would delete an entry carrying its own `last_run` and `fail_count` history.
**`{root}` is substituted here; `{cli}` and `{repo}` are deliberately left in the value.** The CLI list has to be detected and the repositories have to be scanned, and neither is known at seeding time. Expanding into several entries instead would give one `spec_key` several files — the reconcile treats that as `dup=` and refuses to touch any of them — and would go stale the moment a CLI is installed or a repository cloned, with re-expansion costing the history it just protected. So the hole stays, and one rule pays for it: **an `action` containing a brace is not yet a runnable command and must never be executed as written.** Nothing executes an `action` today — `tasks.sh` stores it, `due.sh` prints it, `status` and the monitor page print it — so the rule is aimed at whoever wires execution up later: substitute `{cli}` with each detected CLI token and `{repo}` with each scanned repository working directory, run once per target, and let the targets' results together count as this entry's one success or failure.
**A `pending` row stays a reminder but is never reported as an ordinary one.** The reason lives in the report, as a `pending=` line, and nothing is written into the task file. Putting the marker in the title would change the `id` and cut that entry's history, and the drift check compares `kind`, `action`, `trigger` and `recur` but not the title, so a stale marker would never be caught; adding a sixteenth key would change the task book's fixed storage format for a piece of upstream prose the task book has no way to edit later. `status` runs `plan`, so `pending=`, `held=` and `drift=` all reach a human in the same place.
**Neither repository's merge order can break the other.** `seed-tasks.sh` measures the list's column count once: 12 or more and `probe` applies, 11 or fewer and every non-`invoke` row seeds as `remind` exactly as before, with a single note saying why rather than one warning per row. On the round after `jsc-meta` gains the column, the four command rows show up as `drift=` and change nothing until a human passes `--refresh` — report that as the expected transition, not as a fault.
**Seeding and rebuilding are the same call.** There is no first-run flag and no second code path: what happens is decided by comparing the list against the task book, so the first call seeds every item and every later call is a no-op until the list actually changes. That is why `start` runs it every time rather than only once. **Seeding and rebuilding are the same call.** There is no first-run flag and no second code path: what happens is decided by comparing the list against the task book, so the first call seeds every item and every later call is a no-op until the list actually changes. That is why `start` runs it every time rather than only once.
**A `cond` row is held back on purpose, and the report has to say so.** The condition lives in prose in the list, so no script can evaluate it, and one of them says in as many words that its own scripts still resolve through version-carrying paths — seeding it would have the assistant invoke, every single round, something that stops on its first script call with no error, no output and a heartbeat that still looks healthy. So the default is to hold, and every held row is printed with its condition and the half that stays with a human. Never quietly drop them: a missing item nobody can account for is the failure this reporting prevents. **A `cond` row is held back on purpose, and the report has to say so.** The condition lives in prose in the list, so no script can evaluate it, and one of them says in as many words that its own scripts still resolve through version-carrying paths — seeding it would have the assistant invoke, every single round, something that stops on its first script call with no error, no output and a heartbeat that still looks healthy. So the default is to hold, and every held row is printed with its condition and the half that stays with a human. Never quietly drop them: a missing item nobody can account for is the failure this reporting prevents.
@@ -251,7 +270,7 @@ The delegation list holds one row per jsc skill and records whether that skill c
| Code | Meaning | What to do | | Code | Meaning | What to do |
| --- | --- | --- | | --- | --- | --- |
| 0 | The two sides are reconciled — `plan` printed its verdict, or `apply` made the changes. Zero changes is this code too | Carry on, and carry the `added=`, `removed=`, `kept=`, `drift=`, `held=` and `bad=` counts into the report | | 0 | The two sides are reconciled — `plan` printed its verdict, or `apply` made the changes. Zero changes is this code too | Carry on, and carry the `added=`, `removed=`, `kept=`, `drift=`, `held=`, `bad=`, `probe=`, `pending=` and `probe_bad=` counts into the report |
| 1 | The delegation list could not be read, so **nothing was touched** | Report `jsc-meta` as missing or unreadable and say the built-in items were left exactly as they were. Never report this as "the list has no delegable skills" | | 1 | The delegation list could not be read, so **nothing was touched** | Report `jsc-meta` as missing or unreadable and say the built-in items were left exactly as they were. Never report this as "the list has no delegable skills" |
| 2 | The list was read but not one delegable row came out of it, so **nothing was touched** | Report the list itself as suspect — a half-synced or damaged list would otherwise delete every built-in item. Point at the file and stop | | 2 | The list was read but not one delegable row came out of it, so **nothing was touched** | Report the list itself as suspect — a half-synced or damaged list would otherwise delete every built-in item. Point at the file and stop |
| 3 | `tasks.sh` was not found, so **nothing was touched** | Report the installation as incomplete: the task book has exactly one writer and it is missing | | 3 | `tasks.sh` was not found, so **nothing was touched** | Report the installation as incomplete: the task book has exactly one writer and it is missing |
@@ -282,7 +301,7 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
`start` proves the loop works before it schedules it: the built-in items first, then one patrol round, then the scheduled entry. It installs no daemon and writes no bare heartbeat. `start` proves the loop works before it schedules it: the built-in items first, then one patrol round, then the scheduled entry. It installs no daemon and writes no bare heartbeat.
1. **Reconcile the built-in check items against the delegation list.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh apply --root {CURRENT}`. This comes before the round, so the round's own task-book section already shows the items this machine is supposed to be checking. Judge the result by the seed-tasks.sh exit-code table, and keep every `add=`, `remove=`, `drift=`, `held=`, `bad=`, `dup=` and `skip_user=` line plus the summary counts for the report. **Exit 1, 2 and 3 do not stop the start.** Nothing was touched in any of those cases, so the assistant still has whatever items it had before and the round is still worth running: record what the code means, put it into the closing report, and carry on to step 2. Exit 4 is the same — the entries that did get added or removed stand, and the failed ones are named. Never pass `--force` and never pass `--allow-cond` on your own initiative: the first would let this step delete something a person asked for, and the second asserts a condition only a person can check. Completion condition: the exit code and the summary counts are recorded, with every `held=` row's skill name kept for the report, or the code was recorded as "nothing was touched" and step 2 was reached anyway. 1. **Reconcile the built-in check items against the delegation list.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh apply --root {CURRENT}`. This comes before the round, so the round's own task-book section already shows the items this machine is supposed to be checking. Judge the result by the seed-tasks.sh exit-code table, and keep every `add=`, `remove=`, `drift=`, `held=`, `bad=`, `dup=`, `skip_user=`, `probe=`, `pending=` and `probe_bad=` line plus the summary counts for the report. **Exit 1, 2 and 3 do not stop the start.** Nothing was touched in any of those cases, so the assistant still has whatever items it had before and the round is still worth running: record what the code means, put it into the closing report, and carry on to step 2. Exit 4 is the same — the entries that did get added or removed stand, and the failed ones are named. Never pass `--force` and never pass `--allow-cond` on your own initiative: the first would let this step delete something a person asked for, and the second asserts a condition only a person can check. Completion condition: the exit code and the summary counts are recorded, with every `held=` row's skill name kept for the report, or the code was recorded as "nothing was touched" and step 2 was reached anyway.
2. **Run one patrol round.** Follow every step of the `patrol` operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (`collect` exit 4, 5 or 6, an empty `hash=`, a failed write of the monitor page, a directory-entry failure other than exit 3, or `finish` exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 4, and do not claim a started assistant. A round that completed with failed items (`collect` exit 1 or 3) is still a completed round — carry on to step 3 and name the failures in the closing report. Completion condition: `patrol.sh finish` exited 0, or the failure report naming the step and the code has been printed and no start was claimed. 2. **Run one patrol round.** Follow every step of the `patrol` operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (`collect` exit 4, 5 or 6, an empty `hash=`, a failed write of the monitor page, a directory-entry failure other than exit 3, or `finish` exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 4, and do not claim a started assistant. A round that completed with failed items (`collect` exit 1 or 3) is still a completed round — carry on to step 3 and name the failures in the closing report. Completion condition: `patrol.sh finish` exited 0, or the failure report naming the step and the code has been printed and no start was claimed.
@@ -292,7 +311,7 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
5. **Report the start.** Print the round's verdict and its four item results, the monitor page that was written, the heartbeat path, the local time of `ts`, the TTL in seconds, `pid`, `cli` and `session` as hints, then the scheduler mechanism, the derived period, the installed entry line as the script printed it with the token already masked, the `patrol_root=` the entry carries — that is what every later round reads its tool root from — how many legacy heartbeat entries were removed, and how many other entries were left untouched. Then hand over the two operator items the install printed: the `allow_rule=` lines verbatim, so the unattended round never meets a permission prompt, and the reminder that the entry holds a snapshot of the listed variables including the token — keep the crontab file readable by its owner alone, and run `install` again after any of those variables changes. 5. **Report the start.** Print the round's verdict and its four item results, the monitor page that was written, the heartbeat path, the local time of `ts`, the TTL in seconds, `pid`, `cli` and `session` as hints, then the scheduler mechanism, the derived period, the installed entry line as the script printed it with the token already masked, the `patrol_root=` the entry carries — that is what every later round reads its tool root from — how many legacy heartbeat entries were removed, and how many other entries were left untouched. Then hand over the two operator items the install printed: the `allow_rule=` lines verbatim, so the unattended round never meets a permission prompt, and the reminder that the entry holds a snapshot of the listed variables including the token — keep the crontab file readable by its owner alone, and run `install` again after any of those variables changes.
**Then report step 1's reconcile in its own block**, because it is the only place the built-in items are accounted for: how many were added, how many removed, how many left alone, then every held `cond` row by name with the reason it was held, every `bad=` row as a defect in the list rather than in this machine, every `drift=` row with the change the list now asks for and the note that `--refresh` is what applies it, and every `dup=` or `skip_user=` row as an entry a person has to settle. An exit of 1, 2 or 3 is reported here as "the built-in items were left as they were" with the reason, never as "there is nothing to check". Close with the notice that matches step 4's outcome, printed literally with `{ttl}` replaced by the TTL just read and `{period}` by the derived period: **Then report step 1's reconcile in its own block**, because it is the only place the built-in items are accounted for: how many were added, how many removed, how many left alone, then every held `cond` row by name with the reason it was held, every `bad=` row as a defect in the list rather than in this machine, every `drift=` row with the change the list now asks for and the note that `--refresh` is what applies it, and every `dup=` or `skip_user=` row as an entry a person has to settle. **Then account for the `probe` column separately**: every `probe=` row as an item that now runs a read-only command rather than only reminding, naming any `{cli}` or `{repo}` hole still in it; every `pending=` row as a slice whose entry point is not wired yet, with the list's own reason — that is the only place those are distinguishable from the rows that were always reminders; and every `probe_bad=` row as an item that fell back to reminding, with what stopped the substitution. When the summary carries `spec_cols=11`, say that this machine's `jsc-meta` predates the column and that no item runs a command this round. An exit of 1, 2 or 3 is reported here as "the built-in items were left as they were" with the reason, never as "there is nothing to check". Close with the notice that matches step 4's outcome, printed literally with `{ttl}` replaced by the TTL just read and `{period}` by the derived period:
| Step 4 | Notice | | Step 4 | Notice |
| --- | --- | | --- | --- |
@@ -394,7 +413,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
6. **Flag the repeatedly failing tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Completion condition: every row with `fail_count` above 0 carries the marker and its number matches the file. 6. **Flag the repeatedly failing tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Completion condition: every row with `fail_count` above 0 carries the marker and its number matches the file.
7. **Check the built-in items against the delegation list, read-only.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh plan --root {CURRENT}`. `plan` writes nothing at all — it prints what a reconcile would do and stops — which is what makes it safe here, and `apply` must never be run from `status`. Judge the code by the seed-tasks.sh table and report the difference: the count of items the list expects but the task book lacks, the count of orphans the task book still holds, every `held=` row by name, and every `drift=` row with the change the list asks for. Say plainly that `start` is what applies any of it. On exit 1, 2 or 3 report that the comparison could not be made and why, and never present that as an aligned task book. Completion condition: the difference is reported with its counts and the held rows named, or the reason it could not be computed is reported, and nothing under `$JSC_HOME` was written. 7. **Check the built-in items against the delegation list, read-only.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh plan --root {CURRENT}`. `plan` writes nothing at all — it prints what a reconcile would do and stops — which is what makes it safe here, and `apply` must never be run from `status`. Judge the code by the seed-tasks.sh table and report the difference: the count of items the list expects but the task book lacks, the count of orphans the task book still holds, every `held=` row by name, every `drift=` row with the change the list asks for, every `pending=` row as a slice with an entry point still unwired — this is where a human finds out which reminders are waiting on plumbing rather than on them — and every `probe_bad=` row as an item that fell back to reminding. Say plainly that `start` is what applies any of it. On exit 1, 2 or 3 report that the comparison could not be made and why, and never present that as an aligned task book. Completion condition: the difference is reported with its counts and the held rows named, or the reason it could not be computed is reported, and nothing under `$JSC_HOME` was written.
8. **Finish successfully.** `助理未運行`, an absent `tasks/` directory, an empty `tasks/` directory and an uninstalled schedule are normal results — never exit non-zero for any of them. Reserve a failure report for a condition none of the tables above covers, and state which path and which error produced it. Completion condition: the report is printed and nothing under `$JSC_HOME` has been created, modified or deleted. 8. **Finish successfully.** `助理未運行`, an absent `tasks/` directory, an empty `tasks/` directory and an uninstalled schedule are normal results — never exit non-zero for any of them. Reserve a failure report for a condition none of the tables above covers, and state which path and which error produced it. Completion condition: the report is printed and nothing under `$JSC_HOME` has been created, modified or deleted.
+234 -24
View File
@@ -41,13 +41,13 @@
# #
# --- 哪些欄位從清單來、哪些自己補 --- # --- 哪些欄位從清單來、哪些自己補 ---
# #
# 清單有十一欄,待辦簿有十五個鍵,對得上的只有兩欄。逐個交代: # 清單有十二欄,待辦簿有十五個鍵,直接抄過來的只有兩欄。逐個交代:
# 從清單直接抄過來 # 從清單直接抄過來
# trigger 原樣抄。第一次什麼時候到期是判定結果,不是這一支的決定 # trigger 原樣抄。第一次什麼時候到期是判定結果,不是這一支的決定
# recur 原樣抄。同上 # recur 原樣抄。同上
# 從清單推出來 # 從清單推出來
# spec_key domain 與 name 兩欄合起來寫成 jsc-{domain}:{技能名},那就是一支技能的身分 # spec_key domain 與 name 兩欄合起來寫成 jsc-{domain}:{技能名},那就是一支技能的身分
# action 由 way 推,對映見下一段。way 與 action 不是同一件事 # action 由 way 與 probe 兩欄一起推,對映見下一段。way 與 action 不是同一件事
# title 固定寫成「委派清單內建項:{spec_key}」。標題刻意不含 way、trigger、recur: # title 固定寫成「委派清單內建項:{spec_key}」。標題刻意不含 way、trigger、recur:
# 那幾欄會隨清單改動而變,寫進標題就等於每一次改動都換一個 id,而 id 一換, # 那幾欄會隨清單改動而變,寫進標題就等於每一次改動都換一個 id,而 id 一換,
# last_run 與 fail_count 的歷史就跟著斷掉 # last_run 與 fail_count 的歷史就跟著斷掉
@@ -68,19 +68,92 @@
# 兩個同名不同義,一律不互抄 # 兩個同名不同義,一律不互抄
# state、last_run、next_run、fail_count 由 tasks.sh 與判到期那一邊維護,這一支不碰。 # state、last_run、next_run、fail_count 由 tasks.sh 與判到期那一邊維護,這一支不碰。
# #
# --- way 與 action 的對映 --- # --- way 與 probe 怎麼推出 action ---
# #
# 清單的 way 是交出方式,三種:invoke 觸發、patrol 巡檢、remind 提醒。待辦簿的 action 是 # 清單的 way 是交出方式,三種:invoke 觸發、patrol 巡檢、remind 提醒。待辦簿的 action 是
# 助理實際要跑的事,三種:技能名、腳本、remind。兩套詞彙不對應,所以要明寫對映: # 助理實際要跑的事,三種:技能名、指令、remind。兩套詞彙不對應,所以要明寫對映:
# way 含 invoke → action 取技能名。觸發的定義就是呼叫既有技能、內容照那支技能自己的 # way 含 invoke → action 取技能名,probe 一律不看。觸發的定義就是呼叫既有技能、內容
# 流程走,所以填技能名就是照判定結果做 # 照那支技能自己的流程走,所以填技能名就是照判定結果做
# 其餘(patrol、remind、patrol,remind)→ action 取 remind # 其餘那幾種(patrol、remind、patrol,remind)看 probe 那一欄:
# 第二條的理由要講清楚,因為它看起來像偷懶。巡檢那個交出方式的意思是「助理在自己那一輪裡 # probe 是一行指令 → action 取那一行指令,代入點先代好
# 順手做那一段唯讀盤點」,而那一段沒有獨立的入口:巡檢那一輪讀的是固定那幾項來源,沒有 # probe 是 pending:{理由} → action 取 remind,另外印一行 pending=
# 一支腳本或一個技能名代表得了「某一支技能的唯讀切片」。這時候把技能名填進 action,助理下一輪 # probe 是減號或空的 → action 取 remind
# 就會去呼叫整支技能,那正是切片交要防的事——留在人手上的那一半會被一路跑完。 # 中間那一條原本沒有:這一支以前把非 invoke 的列一律推成 remind,因為那些唯讀切片沒有獨立
# 所以填 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 return 1
} }
# --- way 對 action --- # --- way 與 probe 對 action ---
# 對映與理由見檔頭「way 與 action 的對映」。way 是半形逗號隔開的清單,比對時前後各補一個 # way 是半形逗號隔開的清單,比對時前後各補一個逗號,才不會讓 invoke 去命中一個叫別的名字
# 逗號,才不會讓 invoke 去命中一個叫別的名字但含有 invoke 這幾個字的交出方式。 # 但含有 invoke 這幾個字的交出方式。
action_of() { # $1=way $2=spec_key is_invoke() { # $1=way
case ",$1," in case ",$1," in
*,invoke,*) printf '%s' "$2" ;; *,invoke,*) return 0 ;;
*) printf 'remind' ;;
esac 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_HELD=0
N_BAD=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 case "$c_domain" in
''|'#'*) continue ;; ''|'#'*) continue ;;
esac 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:--}" "$_key" "$c_verdict" "${c_trigger:--}" "${c_recur:--}"
continue continue
fi 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' \ 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 \ "$c_trigger" "$c_recur" >>"$WANT" 2>/dev/null \
|| die 5 "暫存檔寫不進去:$WANT。" || die 5 "暫存檔寫不進去:$WANT。"
done <"$SPEC" done <"$SPEC"
@@ -491,12 +698,15 @@ done <"$HAVE"
# --- 摘要 --- # --- 摘要 ---
N_HAVE=$(awk 'END{print NR+0}' "$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' \ 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" "$TASKS_SH" "$N_WANT" "$N_HAVE" "$N_ADD" "$N_REMOVE" "$N_KEEP" \ "$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_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_HELD" -gt 0 ] && note "有 $N_HELD 支是條件式交,這一次沒有種入,逐支印在上面的 held= 那幾行。條件是散文,程式判不了;人確認過某一支的條件成立就帶 --allow-cond 那一支的鍵。"
[ "$N_BAD" -gt 0 ] && warn "清單上有 $N_BAD 列對不上,那幾支這一次沒有種入,逐列印在上面的 bad= 那幾行。請回去補清單,不要在這裡補預設值。" [ "$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 會歸零。" [ "$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 的結束碼,照那一支的結束碼表處理。" [ "$RC_PARTIAL" -eq 0 ] || die 4 "有 add 或 remove 失敗,其餘各筆照做完了。失敗的逐筆印在上面的 add_failed= 與 remove_failed= 那幾行,各自帶了 tasks.sh 的結束碼,照那一支的結束碼表處理。"