Files
meta/skills/skill-new/SKILL.md
T
jiantw83 f2332f965c feat(delegate): 清單加一欄記唯讀盤點的實際指令
切片交的那幾支,助理現在只會提醒不會動手,因為沒有東西記得下「那一段唯讀盤點到底要跑什麼」。清單加第十二欄記具體指令,種入那一支有值就拿它當動作、沒值才退回只提醒。

只認三種寫法。一行指令,路徑一律寫成代入點開頭,由種入那一支代進字面絕對根目錄;指令裡不可以出現金錢符號或波浪號,那兩種在無人值守那一輪解不出來也進不了允許清單,會被靜靜擋掉。要標未接線就寫理由,留白的話下一輪分不出是刻意還是漏填。沒有唯讀入口的寫減號。

觸發型與不交的列一律填減號。觸發的意思是呼叫整支技能,這一欄填了指令會讓種入那一支改拿指令當動作,於是整支交出降級成只跑一支腳本,該寫的頁一頁都不會寫,而且看起來完全正常。這條由檢核腳本擋。

逐項回各存放庫核對,不照抄既有盤點的措辭,因而抓到五處對不上實際腳本的地方。

最要緊的一處:既有盤點點名的那支工具腳本要三個參數,其中兩個無人值守那一輪根本拿不到,而且它是唯一會去問遠端的一方。改用同一件事的本機那一半,而那個子命令剛好在閘門腳本「免讀標準輸入」的清單裡。這一點非確認不可:同一支腳本的另一個子命令不在那份清單上,標準輸入是管線又沒人關閉時會一直等,實測會無限卡住。腳本自己的註解就記著這個坑,說工具腳本轉呼叫時曾經整支卡死——那正是先前心跳斷掉的同一種死法。

另一處:既有盤點點名的同步子命令會寫檔,不是唯讀。那一列剛好是觸發型所以填減號、衝突沒落地,但措辭本身是錯的。

七項標成未接線,理由都是要連網要金鑰。連網那一種一過期就讓那一項每輪失敗,或每輪靜靜回報沒事——後者更難查;而純本機讀取本來一輪都不會失敗。先填會連網的,等於用一批每輪報錯的項目把真的發現蓋掉。

還記下一件接線那天要注意的事:查遠端版本那一支永遠回成功,查不到就安靜放行,所以金鑰失效時它會靜靜回報沒有新版。接線要用另一個子命令,那個有「查不出來」這第三種結論。

檢核腳本從四項檢查改成五項。填錯欄位、指到不存在的腳本、用了認不得的代入點都算缺失;標未接線只印提示,那是判過知道還沒接,不是漏填;指到的 domain 本機沒裝也只提示,那是機器少裝一套不是清單填錯,算缺失會讓半套機器每輪亮紅。

五支技能異動流程一併改:寫清單時要問到並寫下這一欄,重判時要一併重問,刪除時要檢查別列有沒有指到一起刪掉的腳本,一次改多支時要注意跨存放庫搬腳本是這一欄最容易過期的地方。
2026-09-04 09:56:55 +08:00

24 KiB
Raw Blame History

name, description
name description
skill-new Create a new skill in the jsc skill set. Prefetch the skill list and the domain list in parallel, ask skill details via decision tree, generate the skill under the right jsc-{domain} per guidelines.md (create the domain from the template repo if missing), open a PR via jsc-git pr, then deploy and verify per references/deploy-verify.md from a fresh CLI process, and append the change report to wiki SKILLSET_{HASH}. Use when the user wants to add a skill; not for editing an existing one (use skill-update).

skill-new — create a skill

Single source of guidelines: ../../references/guidelines.md.

Flow

  1. Collect the decision tree's inputs first, then ask:

    1. Prefetch both inputs — run tools/list-skills.sh and tools/sync-domains.sh in parallel. They share no data, and both answers are needed before the first question, so running them after the questions only makes the user wait. Route each exit code:

      • list-skills.sh exit 0 — keep the domain<TAB>name<TAB>description rows. Exit 1 — the root could not be derived, the domain list was unreadable, or no skill was found; read stderr and fix the named cause. When stderr says the root could not be derived, set JSC_PLUGINS_ROOT to the directory that holds the domain repos and rerun: under a plugin install the script sits in the CLI's plugin cache, so its built-in guess lands in that cache instead of the domain workspace.
      • sync-domains.sh exit 0 — the only code that means every repo is present and current; keep the domain<TAB>path rows. Exit 3 — some repos were not updated: reconcile every path named on stderr (commit or stash the dirty tree, or fix the failing pull) and rerun; when the user confirms a dirty tree is intentional local work, record that decision and continue on the local version. Exit 2 — a domain could not be cloned. Exit 1 — the root could not be derived, gitea.sh was not found, or the canonical marketplace was unreadable; for the root case set JSC_PLUGINS_ROOT as above and rerun. Resolve 2 and 1 before continuing.

      Completion condition: the skill rows and the domain<TAB>path rows are both in hand.

    2. Ask for skill details via the jsc-ask:ask decision tree until no doubt remains:

      • Goal (single and not duplicating an existing skill; show the similar skills from the step 1.1 rows for comparison — options must state the impact scope of "reuse existing" versus "create new")

      • Trigger (when to use, when not to, trigger keywords)

      • Input and output (can a standard input/output flow move down to tools/; does it need Gitea operations — if so, make the skill use jsc-gitea/tools/gitea.sh + token)

      • Owning domain (offer the domain list from the step 1.1 domain<TAB>path rows — the domains registered in the canonical marketplace)

      • Delegation verdict — the five decision-tree questions of ../../references/delegate-criteria.md, in the order that file lists them, plus a sixth question for the next column: which skill should run after this one, and a seventh for the probe column: which read-only command the assistant actually runs for the delegated slice. Ask all seven through this same jsc-ask:ask tree; never answer them from the model's own reading of the draft flow. Every option states its impact scope: a full verdict lets the background assistant run the skill unattended, a slice or cond verdict leaves the other half in the user's hands, none keeps the whole skill there. The next question applies to all four verdicts, none included — none says the assistant does not run this skill for the user, which says nothing about what should follow it — so offer the step 1.1 skill rows as its options and the answer then names a skill that exists.

        The probe question is asked only when the way answer holds no invoke; a way containing invoke, and a none verdict, both take - without asking, because the assistant's action there is the skill itself and a command in that column would silently downgrade the whole delegation to a bare script run. When it is asked, settle three things per that same reference file and never by copying another row: which script, whether it takes a read-only flag, and how a failure is reported. Verify the script, the sub-command and the flag name in the owning repo before writing them down — a description of a script is not evidence the script exists. The flag matters most: the patrol round is unattended, and a wrong flag turns a read-only stocktake into something that writes. An entry that needs the network gets pending:{reason} this round rather than a command, because an expired key then fails or silently reports nothing on every round while a local read fails on none.

      Completion condition: goal, trigger, input/output, owning domain and the delegation verdict each have a recorded answer, and the verdict carries a value for every column delegate-criteria.md marks mandatory for that verdict, - where it marks the column unused.

  2. If the domain does not exist (tools/sync-domains.sh clones every domain registered in the marketplace, so a missing directory means the domain is unregistered — the repository itself may already exist on Gitea):

    1. Propose one short English word for the new domain (a single word preferred) and confirm it with the user. Completion condition: the user confirms the domain word.

    2. Check before creating: run jsc-gitea/tools/gitea.sh clone-url plugins/{domain}. A URL comes back when the repository already exists — clone it, skip creation, and go on to step 2.3 to fill in whatever content is missing. Only when no URL comes back create the repository through the tool, never by hand: gitea.sh api POST /orgs/plugins/repos when plugins is an organization, POST /user/repos when plugins is the token's own account (tea repo create does the same job). Only when the call is refused (403 — the token has write but not admin rights on the owner) ask the user to create plugins/{domain} by hand, then continue. Completion condition: gitea.sh clone-url plugins/{domain} prints a URL and cloning it succeeds.

    3. Build the content following the structure of https://gitea.jsc.idv.tw/plugins/template: three plugin manifests (plugin name jsc-{domain}, version starting at 0.0.1), skills/, README.md, AGENTS.md. Completion condition: the three manifests, skills/, README.md and AGENTS.md all exist in the new repo.

    4. Register the plugin: run tools/sync-marketplace.sh {domain} {repo-url} {description}. It needs python3 on PATH — it edits the marketplace JSON with the json module. It writes the entry into both canonical marketplace files in plugins/meta and copies both into every domain repo, so any repo works as the registration entry point. Route each exit code:

      • Exit 3 — written, but some domain repo is not present locally. Run tools/sync-domains.sh, then rerun this step.
      • Exit 2 — usage error. Fix the three arguments and rerun.
      • Exit 1 — the root could not be derived, python3 is missing, a canonical file was unreadable, or copies differ byte for byte. Read stderr and fix the named cause: install python3 for the second; for the root case set JSC_PLUGINS_ROOT to the directory that holds the domain repos, because under a plugin install the script sits in the CLI's plugin cache and its built-in guess lands there. Then rerun.
      • Exit 0 — every copy holds identical bytes; the script verifies that itself.

      Completion condition: the script exits 0 and prints the touched paths.

  3. Generate the skill per guidelines.md — this step MUST run as a sub agent:

    • skills/{name}/SKILL.md: entirely in English (description within either cap — ≤ 5 sentences or ≤ 5 steps — and stating when to use and when not to; body in STE100-style English)
    • Rules enforceable by hooks go to jsc-hooks (never scattered in this domain); standard input/output flows go to tools/
    • jsc-meta/tools/delegate-spec.tsv: append this skill's row, built from the step 1.2 verdict — one skill one row, the twelve tab-separated columns in the order that file's header lists, probe last. A column the verdict does not use holds a single -; an empty cell and a cell holding a space both fail the checker. Write probe as the header describes: the script path starts at {root}/jsc-{domain}/, {cli} and {repo} are the only other substitution points, no dollar sign and no tilde, and any read-only flag goes in front of the command. origin is judged, because the verdict came from the decision tree in this same run, and version is the version the three manifests carry after the sync-skill-manifest.sh run below. A skill with no row is not created. The row is the only thing that tells the background assistant this skill exists, so without it every later round is blind to it, and no later step recreates it. The file lives in plugins/meta whichever domain gained the skill, so a skill added to another domain changes two repos and step 5 opens the second Push Request for this one.
    • references/behaviors.md: add one ## {name} section for the new skill, placed in dictionary order among the existing sections, carrying the five rows the guidelines' 「技能行為清單」 section defines — 觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象. Write what the skill really does; do not copy the description. A read-only skill still fills 可驗證跡象 with 「無寫入跡象,只有回報內容」. The behavior list ships in this same PR — a skill added without its section leaves the domain's list out of sync the moment this PR merges. When the domain has no references/behaviors.md yet, create it with the header line # jsc-{domain} 技能行為清單.

    Then run tools/sync-skill-manifest.sh {domain-path} directly (no sub agent needed) to sync the domain README's 「Skills 目錄」 section and bump the version in all three manifests. Route each exit code: 0 — the README block and all three manifests are synced; 1 — the domain path, skills/, README.md, the JSC-SKILLS markers, a SKILL.md, a manifest, or a manifest version field is missing, so fix the named cause on stderr and rerun; 2 — usage error, the script takes exactly one argument; any other code — the script runs under set -e, so treat it as an environment fault and stop, never as a successful sync. Completion condition: skills/{name}/SKILL.md exists, references/behaviors.md holds a ## {name} section with all five rows filled, tools/delegate-spec.tsv holds this skill's row with every mandatory column filled, the README lists the skill, and all three manifests show the same new version.

  4. Self-check every item of the guidelines.md audit checklist; fix anything that fails. Run tools/check-behaviors.sh {domain-path} for the behavior-list item instead of comparing by eye, and route each exit code: 0 — the list matches skills/ and all five rows are filled; 1 — every mismatch is printed on stderr as {檔案}:{技能名}:{說明}, so fix each one and rerun; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because references/behaviors.md is missing, skills/ is missing, or no SKILL.md was found, so create the missing file and rerun. Exit 3 is never a pass.

    Then run tools/check-delegate.sh for the delegation-list item. It takes the plugins root, not a domain path, and the list is one file covering every domain, so it runs once for the whole flow — a second run per domain checks the same file again and reports the same lines. Route each exit code:

    • 0 — the list matches the skills on this machine and every mandatory column is filled. A run that printed lines on stdout and exited 0 still passed. Those lines are hints, not defects: origin=seed marks a row seeded from the earlier inventory and awaiting review, and a version-behind line marks a row whose version trails its domain's current one, a probe=pending: line marks a delegated slice whose read-only entry point is not wired yet, and a line saying a probe domain is not installed here marks a script this machine cannot check. The version number is per domain, so bumping one skill's domain marks every other skill in it — reading those lines as failures paints the whole domain red on every release until nobody reads them at all. Report the hints, fix nothing for them, and treat this item as passed.
    • 1 — a missing row, a duplicate row, a row for a skill this machine does not have, an empty column, a column value outside its vocabulary, a next pointing at a skill that does not exist, or a probe in the wrong shape — a command on a row whose way holds invoke, a - on a row whose way holds only patrol or remind, a dollar sign or tilde, an unknown substitution point, or a script that does not exist. Every one is printed on stderr as {清單路徑}:{domain}/{技能名}:{說明}; fix each and rerun. The new skill's own missing row is the expected finding when step 3 skipped its write, and the fix is that write, not an edit here.
    • 2 — usage error: the script takes at most one argument. Fix the call and rerun.
    • 3 — nothing was checked, because tools/delegate-spec.tsv is missing, the root could not be derived, or list-skills.sh listed no skill. Read stderr and fix the named cause; set JSC_PLUGINS_ROOT to the directory holding the domain repos for the root case, as in step 1.1. Exit 3 is never a pass — it means the check looked nowhere, so a new skill with no row would sail through it.

    Completion condition: every checklist item passes, tools/check-behaviors.sh {domain-path} exits 0, and tools/check-delegate.sh exits 0 with the new skill's row present, its hint lines reported as hints.

  5. Call jsc-git:pr to open a Push Request. When the new skill went into a domain other than meta, the tools/delegate-spec.tsv row is a change to plugins/meta and needs its own Push Request against that repo — two repos changed, two PRs, neither waiting on the other. Completion condition: a PR URL comes back for every repo this run changed, plugins/meta included when the row landed there, and each is reported with the table format in ../../references/pr-report.md.

  6. Deploy the new skill, verify it runs, then report:

    1. Follow ../../references/deploy-verify.md from section 1 to section 5: tools/deploy-route.sh {domain-path} picks the route, the deploy route or the worktree route runs, and the verification then runs in a fresh CLI process, never in the session that ran the deploy. That session raised the restart gate itself and still holds the old skill body, so verifying inside it either gets blocked or passes on stale behavior. Verify the added skill's row in tools/list-skills.sh, every tool the skill added, and one minimal prompt per checkable CLI — the per-CLI prompts run in parallel. Completion condition: every completion condition in deploy-verify.md sections 1 to 5 holds for this domain repo.

    2. Write the change report to the wiki — this part MUST run as a sub agent. It is two pages in two repos, and they must not be mixed up.

      • Content page SKILLSET_{HASH}. Resolve its repo with jsc-gitea/tools/gitea.sh wiki-repo SKILLSET, which reads JSC_WIKI_REPO_SKILLSET first, then JSC_WIKI_REPO. {HASH} is gitea.sh hash-id "{owner}/{repo}" of the domain repo that gained the skill, used at the full 40 characters it prints. Write it through jsc-gitea:wiki following ../../templates/skillset-page.md: append a section for this change — date, 「新增」, skill name, changed files, PR URL, the step 6.1 route verdict and verification result per item — and keep every earlier section.

      • Directory page SKILLSET_CONTENTS. It lives in the CONTENTS repo, never in the SKILLSET one. wiki-contents.sh resolves it itself with gitea.sh wiki-repo CONTENTS, whose chain is JSC_WIKI_REPO_CONTENTS then JSC_WIKI_REPO and never falls back to JSC_WIKI_REPO_SKILLSET. That page is a heading-plus-bullets list and holds no markdown table: one ## SKILLSET_{HASH} block per domain repo, every field one - {欄位名}:{值} line under it. Build one file holding this domain's single block, following ../../templates/skillset-contents.md, with its 異動頁 bullet written as [SKILLSET_{HASH}]({url}) from the absolute URL that gitea.sh wiki-url {SKILLSET repo} SKILLSET_{HASH} prints. The H2 heading itself carries no link, no URL, no affix and no date — only the content page name. Every link on both pages takes that [{text}]({url}) form; the double-bracket wiki-link form resolves only inside one wiki, so it is never used. Then run:

        jsc-gitea/tools/wiki-contents.sh upsert SKILLSET 2 "SKILLSET_{HASH}" {entry file} templates/skillset-contents.md

        The key is the H2 heading SKILLSET_{HASH}, so one domain keeps exactly one block and no other domain's block moves. That page name depends only on {owner}/{repo}, which is why it is the key: a host rename or a changed JSC_WIKI_REPO_SKILLSET leaves it untouched, so the match still finds the existing block. The 2 is the key column: the index of the column that held the content-page link in the old markdown table, and it matters only when such an old table still has to be converted automatically — the conversion takes the last path segment of that column's link URL as the H2 heading. Count that index from the live page's own column layout, never from the template's: the live SKILLSET_CONTENTS reads | 存放庫 | 異動報告 | 目前版本 | 最後更新 |, so the link sits in column 2 while column 1 is plain text like plugins/ask. Passing 1 would make the heading plugins/ask, which never matches the key SKILLSET_{HASH}, so the existing entry is appended as a brand-new one — one domain ends up with two blocks and the older one is never updated again. The fourth argument is the whole block, not a table row. Never hand-edit the directory page. Write the content page first and fetch the URL only after it exists.

      • Check the links before writing. Hand every URL going onto the content page and into the directory block to jsc-gitea/tools/link-check.sh, and write only when it exits 0. It verifies through the Gitea API, never a web status code: a private repo answers 404 to an unauthenticated web request, so a status-code check would call a live page dead.

      • Exit codes. Route every one of them:

        Call Exit Do
        gitea.sh wiki-repo 2 The page type was misspelled. Fix the argument and rerun
        3 No wiki repo is configured for that type. Name the variable (JSC_WIKI_REPO_SKILLSET for the content page, JSC_WIKI_REPO_CONTENTS for the directory page) and JSC_WIKI_REPO, ask per the jsc-ask:ask rules, then rerun
        gitea.sh hash-id 1 No SHA-1 helper on this machine. Stop and report that sha1sum or shasum has to be installed, and never hand-compute the hash
        2 Empty input, so the {owner}/{repo} was never resolved. Fix that first
        Content page read 0 Append into the sections already there
        4 The page does not exist yet, so build it from templates/skillset-page.md
        7 or 8 Stop and write nothing: a page rebuilt on top of an unread read loses every section already on it
        gitea.sh wiki-url 4 The content page is not there, so the write above did not succeed. Go back and write it, and add no directory block until the page exists
        5 The API answered with no html_url. Stop and report it; never assemble the URL by hand from the host and the page name
        link-check.sh 0 Every link is reachable. Write the page
        1 At least one link is dead. Write nothing, and report the DEAD lines it printed
        2 No URL was passed, which is a defect here. Pass the links and rerun
        3 GITEA_HOST is unset. Set it and rerun; never skip the check instead
        7 Gitea authentication failed. Stop and report the key problem, and never read it as a dead link
        wiki-contents.sh upsert 0 The block is in place. Report the updated or added it printed
        1 The page content could not be assembled, or the write failed. Report SKILLSET_CONTENTS as not written, together with the block content
        2 An argument was rejected. Fix it and rerun; nothing was written
        3 No CONTENTS wiki repo is configured. Report JSC_WIKI_REPO_CONTENTS and JSC_WIKI_REPO as the two variables to set; the new section is on SKILLSET_{HASH} and stays there
        4 The directory page is absent and no template was passed. Rerun with templates/skillset-contents.md as the fifth argument
        7 The token is invalid or lacks permission, so the other domains' blocks are unknown. Stop, report the token problem, and create no page
        8 Some other API failure. Stop, report that status, and create no page

      On any failure, hand the page name and the unwritten entry back to the user and leave this step open; never close the flow on an unwritten report. Completion condition: SKILLSET_{HASH} holds the new section plus all earlier sections, and wiki-contents.sh upsert exited 0 with this domain's ## SKILLSET_{HASH} block on SKILLSET_CONTENTS linking that page by absolute URL.

  7. Report this run's outcome to the local event stream — the last step of every run, the ones that stop early included. Run:

    jsc-hooks/tools/report-status.sh skill-end jsc-meta:skill-new {status} {exit code} [detail]

    Resolve jsc-hooks from the domain<TAB>path row step 1.1 printed for the hooks domain, the same way this skill resolves every other cross-plugin script. When that script is not on this machine, skip this step in silence and close the run as normal. A reporting path that is absent must never fail the run it reports on, and this call's own exit code never changes what this skill reports.

    Pick {status} from what the run actually did:

    status Use it when
    ok the new SKILL.md, its behavior-list section and its delegate-spec.tsv row are in place, the checklist passes, every PR this run needed is open, deploy-verify.md sections 1 to 5 hold, and both wiki writes exited 0
    blocked a gate or a missing prerequisite stopped the run before any file was created — sync-domains.sh never reached exit 0, or Gitea refused the repository creation and nobody created it by hand
    failed the run broke mid-way — sync-marketplace.sh or sync-skill-manifest.sh kept failing, or a wiki write failed again after its one retry
    degraded the skill landed with a part missing — the content page was written while its SKILLSET_CONTENTS block was not, the skill's PR is open while the delegate-spec.tsv PR is not, or a CLI could not be verified and the reason was recorded
    aborted the user stopped the run, or a prerequisite turned out not to hold and this skill stopped on its own

    {exit code} is this run's own result as a number: 0 for ok, non-zero otherwise. detail is optional, one line, at most 200 characters.

    The matching skill-start comes free from the hook, which fires when the skill loads. The creation itself happens in the model turns after that, so no hook can see how the run ended — a start with no end reads as an abort, which is why writing the end is this skill's own job.

    Completion condition: one skill-end line for this run is appended to $JSC_HOME/usage/events.jsonl, or the script was absent and the final report says so.