From e8bf4ddaaf722e783998c41965bf7fc5b648e95b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 31 Aug 2026 11:09:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20=E5=88=86=E9=96=8B=E3=80=8C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8D=E5=90=88=E6=A0=BC=E3=80=8D=E8=88=87?= =?UTF-8?q?=E3=80=8C=E8=85=B3=E6=9C=AC=E8=A2=AB=E5=8F=AB=E9=8C=AF=E3=80=8D?= =?UTF-8?q?=E7=9A=84=E7=B5=90=E6=9D=9F=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit model-tags.sh 的 gate 原本用同一個結束碼表示兩件事:模型缺能力標籤, 以及這支腳本被叫錯。delegate 讀到用法錯誤時,會當成模型沒通過檢查, 默默把一個能用的模型丟掉。真正的錯在哪,永遠不會浮出來。現在用法錯誤 改走另一個碼,兩種「無法判定」也歸到同一個碼,呼叫端只要認碼就分得出 三種結果。model-config.sh 照同一套規則調整,兩支腳本的契約才一致。 check-requires.sh 在沒有 python3 的機器上,會直接讓 shell 回一個沒宣告過 的碼,呼叫端讀不到原因。現在先確認 python3 在不在,並印出擋下的理由, manifest 相依檢查才是真的做得到的事。 delegate 的七個步驟原本沒有任何完成條件,引用了不存在的 plugin,也把 CLI 偵測與標籤篩選寫成文字敘述,可是這兩件事早就有腳本負責。挑模型那 一步需要模型 id,全篇卻沒有任何步驟產得出來。現在每一步都寫出完成條件, 資料一律取自腳本,模型夠不夠格由結束碼判定,不讓模型自評標籤。 setup 的環境變數重驗原本去讀目前這個 shell。可是寫進 rc 檔的值,要等新的 shell 起來才存在,所以重驗永遠回報「沒設到」,把修好的項目誤判成失敗。 現在只驗 rc 段落裡確實有那一行,環境層面交給下一次體檢。 --- skills/delegate/SKILL.md | 112 +++++++++++++++++++++++++++++++-------- skills/setup/SKILL.md | 57 ++++++++++++++++---- tools/check-requires.sh | 17 +++++- tools/model-config.sh | 4 +- tools/model-tags.sh | 9 +++- 5 files changed, 163 insertions(+), 36 deletions(-) diff --git a/skills/delegate/SKILL.md b/skills/delegate/SKILL.md index 068344e..a344539 100644 --- a/skills/delegate/SKILL.md +++ b/skills/delegate/SKILL.md @@ -7,31 +7,99 @@ description: Delegate a single bounded task to another installed AI agent CLI as Use this skill when the work should move to another installed AI agent CLI instead of staying in the current agent. +Detection and tag filtering are decided in code, not in prose: `jsc-cli/tools/detect-clis.sh` owns the CLI list and `jsc-cli/tools/model-tags.sh` owns the capability table. This skill only says when to call them and what each exit code means. + ## Steps -1. Split the request into one bounded goal. One goal means one subagent. -2. Detect the installed AI agent CLIs. Use only a target CLI that is actually present. -3. Choose the execution model. - - If the user names a CLI, keep only that CLI. - - If the user gives capability tags, keep only models that satisfy all tags. - - If the user forces a model, use that exact model and fail if it does not satisfy the tags. -4. Build the subagent prompt. - - Include the goal, the acceptance criteria, the target CLI, the model choice, and the minimum needed context. - - Include `/jsc-shared:spec-output`. - - State the write scope clearly. If no write scope is granted, say the subagent is read-only. -5. Spawn one subagent for the goal. Do not mix unrelated goals in the same subagent. -6. Verify the result before you report it. - - Check the exit status. - - Check that the return value is structured. - - Check that the result covers the acceptance criteria. - - Check that any writes stayed inside the allowed scope. -7. Report the verified result. - - State the CLI, the model, the capability tags, and the outcome. - - Separate success, failure, and anything that still needs user input. +1. Bound the goal. Write one sentence for the goal, and a numbered list of acceptance criteria that a reader can mark pass or fail without asking a follow-up question. + + Two or more independent goals → split them and run this skill once per goal. A goal is not bounded enough when it has no acceptance criterion that can be checked from the subagent's returned text alone. + + Done when exactly one goal sentence and at least one pass-or-fail acceptance criterion are written down. + +2. Collect the CLI list and the model list. They read different files and share no state, so **start both at once and wait for both**. Step 3 has no other source for a model id: without the second script there is nothing to hand to `model-tags.sh gate`. + + `jsc-cli/tools/detect-clis.sh` prints `{name}{path}{version}` per installed CLI. + + | Result | Action | + | --- | --- | + | exit 0, at least one row | Take the candidate CLIs from those rows only | + | exit 0, no row | Stop. Report that no AI agent CLI is installed, and name the five it probes: claude, codex, copilot, antigravity, kiro | + | any non-zero exit | Stop. Report the exit code and the stderr text. Never fall back to a guessed CLI list | + + `jsc-cli/tools/list-models.sh` prints `{cli}{model}{in-use}` per model, read from each CLI's own config, and always exits 0. It stays silent for a CLI whose config it cannot read, so a CLI with no row is a CLI with no known model, not a failure. + + | Result | Action | + | --- | --- | + | exit 0 | Take the candidate models for the target CLI from that CLI's own rows, the `in-use` row first | + | any non-zero exit | The script itself failed. Stop and report the exit code and the stderr text; never invent a model id | + + Done when the candidate CLI list comes from the first TSV and holds at least one name, the model rows from the second are in hand, or the skill has stopped with the reason. + +3. Pick the target CLI, then resolve its model against the requirement. Both read step 2's two TSVs and nothing else, so they belong to one pass over that data. + + **Pick the target CLI first.** The user naming a CLI keeps only that CLI; a name absent from step 2's TSV stops the skill with the message that the CLI is not installed on this machine. No name from the user → keep every detected CLI as a candidate, and settle on one before any model is checked: the candidate model list is defined by the target CLI. + + **Then resolve the model.** Never let a model judge its own tags — the verdict comes from the script's exit code. + + The candidate models are the step 2 `list-models.sh` rows whose first column is the chosen target CLI, checked one at a time in that order, `in-use` first. A user-forced model replaces that list with itself alone. No row for the target CLI and no forced model → stop, and say the fix is to record a model for that CLI through `/jsc-cli:models`; a guessed model id would be gated against a table entry that has nothing to do with the CLI that will actually run the task. + + Requirement stated as an SDLC stage → run `jsc-cli/tools/model-tags.sh gate {stage} {model-id}` per candidate. Exit 2 covers three different faults, so read the output line to tell them apart: + + | Exit | Output | Action | + | --- | --- | --- | + | 0 | `PASS` | Keep the model and stop checking further candidates | + | 1 | `FAIL:{tags}` | Drop that model, name the missing tags in the report, and move to the next candidate | + | 2 | `UNKNOWN-MODEL` | Drop that model and move to the next candidate. Do not guess its tags. Say the fix is to add it through `/jsc-cli:models` | + | 2 | `UNKNOWN-STAGE` | Stop. The stage name is wrong; name the four valid ones: plan, analyze, implement, maintain | + | 2 | usage text on stderr, no verdict line | Stop. The call itself is malformed — report it as a defect in this skill, and never read it as a model that failed the requirement | + | other | — | Stop. Report the exit code and the stderr text | + + Every candidate exhausted without a `PASS` → stop, and report each candidate with its own verdict. + + Requirement stated as raw capability tags → run `jsc-cli/tools/model-tags.sh model {model-id}`, which always exits 0 and prints the model's tags, or prints nothing when the model is not on the table. Keep the model only when its tags cover every required tag. Empty output gets the same treatment as `UNKNOWN-MODEL` above. + + A forced model goes through the same check. Failing it stops the delegation rather than downgrading the requirement. + + Done when exactly one target CLI is chosen and one model id from that CLI has cleared the requirement — a `PASS` from `gate`, or tags covering every required tag — or the skill has stopped with the reason. + +4. Build the subagent prompt. It carries the goal, the acceptance criteria from step 1, the target CLI, the model id, the minimum context needed, the write scope, and the output contract below. + + The write scope is a list of paths the subagent may write, one per line, each an absolute path or a path relative to the current working directory. An empty list means read-only, and the prompt says so in those words. Anything outside the list is out of scope, including temporary files. + + The output contract is a TSV block, and the prompt states it verbatim: + + | Line | Meaning | + | --- | --- | + | `result{ok\|fail\|needs-input}` | Exactly one, and the last line | + | `summary{one line}` | Exactly one | + | `criterion{n}{pass\|fail}{evidence}` | One per acceptance criterion from step 1 | + | `wrote{path}` | One per written path, zero when read-only | + | `error{message}` | One per failure, zero on success | + + Done when the prompt holds all seven parts and the write scope is either a path list or the read-only sentence. + +5. Spawn one subagent for the goal, targeted at the CLI and the model from step 3. One goal, one subagent. Done when the subagent has returned and its exit status has been captured. + +6. Verify the returned result before reporting it. Every check below must pass: + + | Check | Failure handling | + | --- | --- | + | Exit status is 0 | Non-zero → record a failure carrying the target CLI, the exit code and the stderr text | + | Exit status was obtained at all | Not obtainable → treat it as a failure, exactly as a non-zero exit | + | Output holds one `result` line and one `summary` line | Missing either → record a failure reading 回傳格式不符 | + | One `criterion` line per acceptance criterion, all `pass` | Any `fail` or missing line → report the outcome as failure, naming the criteria | + | Every `wrote` path is inside the step 4 write scope | Any path outside → report it as an out-of-scope write and name the path | + + Done when every row above has a verdict, and a failing row has produced a recorded failure. + +7. Report the verified result: the target CLI, the model id, the capability requirement and how it was met, and the outcome. Keep success, failure and 需要使用者補充 in three separate sections, so a partial result is never read as a finished one. + + Done when the CLI, the model, the requirement and the per-criterion verdicts are all in the report, and every failure recorded in step 6 appears in the failure section. ## Do not use this skill -- Do not use it for model inventory. -- Do not use it for plugin deployment. +- Do not use it for model inventory. That is `/jsc-cli:models`. +- Do not use it for plugin deployment. That is `/jsc-cli:deploy`. - Do not use it for tasks that must stay inside the current agent. -- Do not use it when the task is not bounded enough to hand off cleanly. +- Do not use it for a goal that step 1 could not give a pass-or-fail acceptance criterion. diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md index e3a7710..8b04076 100644 --- a/skills/setup/SKILL.md +++ b/skills/setup/SKILL.md @@ -1,6 +1,6 @@ --- name: setup -description: Fix what jsc-cli:doctor found, one confirmed item at a time. Read the 待修項目 table from wiki CHECK_{HASH}, or rebuild it with tools/scan-config.sh and jsc-hooks/tools/wire-cli.sh status when no page exists. Route each item by its fix column - auto writes it through tools/apply-config.sh, ask collects the value through the jsc-ask decision tree first, manual prints the steps for the operator. Delegate compound repairs to their owners: jsc-cli:deploy for a plugin whose version is behind, jsc-hooks:hooks-install for unwired hooks, jsc-cli:models for a missing model-tags.tsv. Re-verify every item after writing and rewrite the CHECK page; use when doctor reports something to fix, not for a read-only checkup. +description: Fix what jsc-cli:doctor found, one confirmed item at a time. Read the 待修項目 table from wiki CHECK_{HASH}, or rebuild it by running the three checkers in parallel and merging them with tools/build-todo.sh. Route each item by its fix column - auto writes it through tools/apply-config.sh, ask collects the value through the jsc-ask decision tree first, manual prints the steps for the operator. Delegate compound repairs to their owners, handing jsc-cli:deploy the mode and the version report it already has. Re-verify every item after writing and rewrite the CHECK page; use when doctor reports something to fix, not for a read-only checkup. --- # setup — guide or apply the fixes doctor found @@ -11,15 +11,27 @@ This skill writes. Every write is confirmed first, backed up, and verified after Read the 待修項目 table from wiki `CHECK_{HASH}` — repo from `jsc-gitea/tools/gitea.sh wiki-repo CHECK`, page name from `gitea.sh hash-id "{hostname}/{user}"`. -No page, or `wiki-repo` exits 3 → rebuild the list here: `tools/scan-config.sh scan all` for settings, `jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI for wiring, `jsc-hooks/hooks/version-guard.sh report` for versions. Rebuilding **MUST run as a sub agent**. +No page, or `wiki-repo` exits 3, or any other non-zero exit from `wiki-repo`, `hash-id` or the wiki read → rebuild the list here. Rebuilding **MUST run as a sub agent**, and its three checkers **start together**: they read different files and share no state, so serialising them only triples the wait. + +| Checker | Command | Exit branching | +| --- | --- | --- | +| Settings | `tools/scan-config.sh scan all` | 0 → use the rows; 2 → usage error, report it as a defect in this skill; 3 → spec table missing, name the path and `JSC_CONFIG_SPEC`; other → report settings as 無法驗證 | +| Wiring | `tools/detect-clis.sh`, then `JSC_READONLY=1 jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI — this step only takes stock, and `wire-cli.sh` without a subcommand rewires, so the read-only contract is carried in the environment rather than trusted to a correctly typed subcommand | detect-clis exit 0 with no row → no CLI to wire, say so and skip; detect-clis non-zero → report wiring as 無法驗證 with the exit code. Per CLI: 0 wired and 1 degraded → nothing to fix; 2 → usage error, defect in this skill; 3 → CLI not installed, drop it; 5 → collect its `missing` items; 6 → readonly refused the call, which means the subcommand was mistyped into a writing one — nothing on the machine changed; fix the command and rerun that CLI; other → report that CLI as 無法驗證 | +| Versions | `jsc-hooks/hooks/version-guard.sh report` | 0 → use the rows, and treat a report with no `{domain}` row or a `noregistry` line as 無法驗證 — other exits → report versions as 無法驗證 with the exit code | + +Merge the three with `tools/build-todo.sh --config {設定輸出} --wiring {cli}={接線輸出} --version {版本輸出}` so the ordering rule lives in one place. Exit 0 → the `todo` rows are the work list; exit 2 → usage error, report it as a defect in this skill; exit 3 → name the unreadable input and rerun that one checker; any other exit → stop and report, because a half-merged list would silently drop a whole class of items. + +Keep the version report from that run. Step 3 hands it to `jsc-cli:deploy` instead of making it query again. State which source the list came from. A stale page and a live scan can disagree, and the operator has to know which one is on screen. -Done when every item carries its scope, verdict and fix route. +Done when every item carries its class, scope, current state and fix route, and the source of the list is named. ## 2. Confirm each item -Ask per the `jsc-ask:ask` decision tree, one item at a time, in the table's order. Every option states its impact scope: which file gets written, which skills start working, what stays broken when skipped. +Ask per the `jsc-ask:ask` decision tree, one item at a time, in the table's order. Confirmation stays strictly sequential: each answer can change what the next item should be, and a batch of questions fired at once takes that away from the operator. + +Every option states its impact scope: which file gets written, which skills start working, what stays broken when skipped. An `ask` item needs its value in the same question — the wiki repo as `{owner}/{repo}`, the Gitea host, the directory path. Never invent one. @@ -35,31 +47,54 @@ Done when every item is either confirmed with a value or recorded as skipped. | `auto` on a directory | `tools/apply-config.sh mkdir {PATH}` | | `ask` | same two commands, with the value the user just gave | | `manual` | print the exact steps and the file to edit; the operator does it | -| domain 落後 | call `jsc-cli:deploy`, mode `update` | +| domain 落後 | call `jsc-cli:deploy` with mode `update` **and the version report from step 1**, so it neither re-asks the mode nor re-queries the versions | | hook unwired | call `jsc-hooks:hooks-install` | | `$JSC_HOME/model-tags.tsv` missing | call `jsc-cli:models` | +`apply-config.sh` exit codes: + +| Exit | Action | +| --- | --- | +| 0 | Written. Record the `wrote` or `created` result and the `backup` path | +| 2 | Usage error — the subcommand, the key or the value is wrong. Record the item as 未修好 with that reason, and do not retry with a guessed argument | +| 4 | Backup or write failed, so nothing was written. Record the item as 未修好 and name the rc file and the stderr, then say the machine is unchanged | +| other | Record the item as 未修好 with the exit code and stderr. Never mark it fixed on an unrecognised exit | + `apply-config.sh` writes into the `# jsc-config` block of every existing shell rc file, backs each one up to `$JSC_HOME/backup/config/{timestamp}/` before touching it, and rewrites the block whole. It never edits anything outside that block. Report the `backup` path it prints. That path is the whole undo story for this run. -Done when every confirmed item has a `wrote`, `created` or delegated result. +Done when every confirmed item has a `wrote`, `created`, delegated or 未修好 result. ## 4. Re-verify -Rerun the check that produced each item — `tools/scan-config.sh scan {scope}` for settings, `wire-cli.sh status {cli}` for wiring, `version-guard.sh report` for versions. +Re-verify every applied item. The items are independent, so **run the re-verifications in parallel** — one batch, one wait. Only the confirmation in step 2 has to stay sequential. + +Pick the check by what was actually written, because the two kinds of write become true at different moments: + +| What was written | Re-verify with | Why this check | +| --- | --- | --- | +| An environment variable in a shell rc file | `tools/apply-config.sh show`, confirming the `KEYVALUE` line is in the `# jsc-config` block | The block is a fact that is already true. The variable reaching the environment is not — a rc file does not touch the running shell | +| A directory | `tools/scan-config.sh scan {scope}`, confirming the row is no longer `missing` or `invalid` | The directory exists the moment it is created | +| Wiring, versions, model tags (delegated) | The owner skill's own returned result | The owner already ran its own verification | + +The same exit branching as step 1 applies to `scan-config.sh` and to `apply-config.sh`. An item that still fails is reported as 未修好 with the reason. Never mark it fixed because the write succeeded: writing the variable and the variable verifying are two different facts. -A newly written rc block does not affect the running shell. Tell the operator to open a new shell or `source` the rc file, and give them the `export` line for the current session. A re-verify that reads the current environment will still show the variable unset — say so rather than reporting a false failure. +For every environment variable written, print the matching `export KEY=VALUE` line for the current session and tell the operator to open a new shell or `source` the rc file. The environment-level proof is handed to the next `/jsc-cli:doctor` run, which starts in a fresh shell — asserting it here would read the shell that could not have picked the value up yet, and report a false failure every time. -Done when every applied item has a fresh verdict from its own checker. +Done when every applied item has a fresh verdict from the checker its own row names, and every environment variable carries its `export` line. ## 5. Record -Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `templates/check-page.md`, and refresh the `CHECK_CONTENTS` row. The page keeps only the latest run, so this overwrites the pre-fix picture on purpose. +Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `templates/check-page.md`. That page is a **content page** and keeps only the latest run, so this overwrites the pre-fix picture on purpose. -No wiki repo configured → report the tables on screen and say the record was skipped. +`CHECK_CONTENTS` is a **contents page** and gets the opposite treatment: read it back first, then upsert this machine's row per `templates/check-contents.md` — add the row if missing, otherwise refresh its counts and 最後體檢. Never overwrite the whole page, and never touch another machine's row. + +The `CHECK_CONTENTS` read branches by exit code, and only exit 4 opens the create path. Exit 0 means upsert into the content that came back. Exit 4 means the page really is not there yet, so build it from the template. Exit 7 (key invalid or no permission) and exit 8 (any other API failure) mean the old rows are unknown, not that the page is missing: skip the `CHECK_CONTENTS` write, name the exit code, and create nothing — the overwrite that is correct for `CHECK_{HASH}` would here destroy every other machine's row, unread and unrecoverable. + +No wiki repo configured, or any non-zero exit from the wiki write → report the tables on screen, say the record was skipped, and name the exit code. Then state the counts: fixed, skipped, delegated, and 未修好. Recommend `/jsc-cli:doctor` for a clean re-check when anything was delegated. diff --git a/tools/check-requires.sh b/tools/check-requires.sh index 262ef4e..6a264ff 100755 --- a/tools/check-requires.sh +++ b/tools/check-requires.sh @@ -1,5 +1,14 @@ #!/usr/bin/env sh -# check-requires.sh — Check jsc.requires before updating one plugin. +# check-requires.sh — 更新單一 plugin 之前,檢查它宣告的 jsc.requires 最低版本。 +# 用法: +# check-requires.sh {claude|codex|copilot|antigravity|kiro} {manifest} +# 輸出(單行,可供程式判讀): +# status=ok reason={沒有宣告相依版本|相依版本符合:...} +# status=blocked reason={缺哪一個 plugin、差哪一版,或 manifest 讀不到} +# 結束碼:0=通過(沒有宣告相依,或全部符合) +# 1=擋下(相依版本不符、缺相依 plugin、manifest 不存在或不是有效 JSON、缺 python3) +# 2=用法錯誤(參數個數不對,或 CLI 代號不在五個之內) +# deploy.sh update 在每個 domain 更新前呼叫一次;擋下就跳過該 domain,不更新到一半才失敗。 set -u usage() { @@ -21,6 +30,12 @@ esac exit 1 } +command -v python3 >/dev/null 2>&1 || { + # 直接讓 shell 回 127 的話,呼叫端會看到一個沒宣告過的結束碼,也讀不到原因。 + printf 'status=blocked reason=找不到 python3,無法解析 manifest:%s\n' "$MANIFEST" + exit 1 +} + JSC_HOME_DIR="${JSC_HOME:-$HOME/.jsc}" LOCAL_DIR="${JSC_LOCAL_PLUGINS:-$JSC_HOME_DIR/plugins}" KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}" diff --git a/tools/model-config.sh b/tools/model-config.sh index c81243e..0aba94d 100755 --- a/tools/model-config.sh +++ b/tools/model-config.sh @@ -10,6 +10,8 @@ # model-config.sh get {stage} 印出該階段解析後的模型鏈(逗號分隔);未設定不印。兩種情況都 exit 0。 # model-config.sh list 每階段一行:stagechainsource(project 或 global);未設定的 chain 與 source 印「-」。 # model-config.sh resolve {stage} 印出該階段「目前 CLI 可用的第一個模型」單一名稱;鏈為空或無法判定時不印,exit 0。 +# 結束碼:0=查詢完成(查得到、查不到都算完成,判斷交給呼叫端) +# 2=用法錯誤(子命令不認得,或階段不在 plan、analyze、implement、maintain 之內) set -u JSC_HOME="${JSC_HOME:-$HOME/.jsc}" @@ -83,7 +85,7 @@ resolve_first_usable() { # $1=階段 usage() { echo "用法:model-config.sh get {plan|analyze|implement|maintain} | list | resolve {plan|analyze|implement|maintain}" >&2 - exit 1 + exit 2 } case "${1:-}" in diff --git a/tools/model-tags.sh b/tools/model-tags.sh index ec1a4ad..ffe2627 100755 --- a/tools/model-tags.sh +++ b/tools/model-tags.sh @@ -16,6 +16,13 @@ # stage{階段}{必要標籤以逗號分隔,不限標籤時為 any} # model{模型鍵}{能力標籤以逗號分隔} # +# 全域結束碼(每個子命令都照這一套,呼叫端只要認碼就好): +# 0 通過或正常完成 +# 1 模型缺標籤(gate 印 FAIL:{標籤}),或 sync 解析不到對照表因而沒寫出檔案 +# 2 用法錯誤:子命令或參數不對(含 UNKNOWN-MODEL 與 UNKNOWN-STAGE 這兩種「無法判定」) +# 「模型不合格」與「這支腳本被叫錯」刻意分成 1 與 2 兩碼:兩者共用 exit 1 的話,呼叫端會把 +# 自己打錯的指令讀成「這個模型不夠格」,把好模型默默丟掉,而且錯在哪永遠不會浮出來。 +# # gate 的輸出與 exit code: # PASS exit 0 模型具備該階段全部必要標籤 # FAIL:{缺少的標籤} exit 1 模型缺標籤,不得執行該階段 @@ -131,7 +138,7 @@ gate() { # $1=階段 $2=模型 id usage() { echo "用法:model-tags.sh dump | sync | stage {階段} | model {模型 id} | gate {階段} {模型 id}" >&2 - exit 1 + exit 2 } case "${1:-}" in