pack-submit
GitHub将本地 Agent Skill 打包为独立 GitHub 仓库,生成清单文件,并向 aeon 社区注册中心提交 PR 以完成发布。支持 dry-run 和自定义仓库名等选项。
Trigger Scenarios
Install
npx skills add aeonfun/aeon --skill pack-submit -g -y
SKILL.md
Frontmatter
{
"name": "pack-submit",
"metadata": {
"var": "",
"mode": "write",
"tags": [
"dev",
"meta",
"packs"
],
"title": "Pack Submit",
"category": "evolution",
"requires": [
"GH_GLOBAL?"
],
"capabilities": [
"external_api",
"writes_external_host",
"sends_notifications"
]
},
"description": "Package one of this agent's own skills as a standalone community pack and submit it to the aeon registry as a PR"
}
${var} — The local skill to publish as a community pack: a skill slug (a directory name under
skills/), optionally followed by flags. Required. Examples:
token-movers— packageskills/token-movers/into a fresh public repo and submit it to the aeon registrytoken-movers --repo myorg/aeon-token-movers— override the pack repo name (defaultaeon-skill-pack-<slug>)token-movers --no-register— create and push the pack repo, but skip the registry PR againstaeonfun/aeontoken-movers --dry-run— build and validate the pack locally, write nothing to GitHub
If ${var} is empty, exit PACK_SUBMIT_NO_VAR:
./notify "pack-submit aborted: var empty — pass a skill slug e.g. \"token-movers\""
Then stop.
Today is ${today}. Your task is to take the existing skill named in ${var}, wrap it in a standalone community-pack repo (its own GitHub repo with a skills-pack.json manifest), and submit it to the aeon community registry - a PR against aeonfun/aeon that adds both surfaces the registry demands in one diff: a row in the Listed packs table in docs/community-skill-packs.md AND a matching entry in catalog/skill-packs.json. This is the inverse of install-skill: instead of pulling a community pack in, it pushes one of your own skills out for every other Aeon agent to install with bin/install-skill-pack.
What a community pack is (so you build the right thing)
A community pack is a public GitHub repo that holds one or more skills plus a skills-pack.json manifest that names and versions them. bin/install-skill-pack owner/repo reads that manifest and installs the skills. To be discoverable (listed by bin/install-skill-pack --list and the dashboard's Community Packs panel), the pack must also be registered in aeonfun/aeon's catalog/skill-packs.json + the Listed packs table in docs/community-skill-packs.md. So publishing is two moves: (a) stand up the pack repo, (b) open the registry PR. This skill does both. Full protocol: docs/community-skill-packs.md.
Steps
-
Parse and validate
${var}. The first whitespace-separated token is the skill slug; the rest are flags (--repo owner/name,--no-register,--dry-run). The slug must match^[a-z0-9][a-z0-9-]*$and resolve to a real directory:SLUG=$(echo "${var}" | awk '{print $1}') FLAGS=$(echo "${var}" | cut -s -d' ' -f2-) if ! echo "$SLUG" | grep -qE '^[a-z0-9][a-z0-9-]*$'; then ./notify "pack-submit aborted: \"$SLUG\" is not a valid skill slug (lowercase kebab-case)"; exit 0 fi if [ ! -f "skills/$SLUG/SKILL.md" ]; then ./notify "pack-submit aborted: skills/$SLUG/SKILL.md not found — run with a slug from \`ls skills/\`"; exit 0 fiIf validation fails, exit
PACK_SUBMIT_BAD_VARwith the notify above and stop. Extract the boolean flags from$FLAGSand the optional--repovalue. -
Read the source skill's metadata. Everything the manifest and registry entry need is already declared in
skills/$SLUG/SKILL.md's frontmatter — read it, don't invent it:- Display name —
metadata.title; fall back to the top-levelname:. - Description — the top-level
description:(one line). - Category —
metadata.category. Registry categories are an open vocabulary, but first-party-only values (core,evolution,basics) don't describe a third-party pack — if you see one of those, pick the closest community category (dev,crypto,productivity,research,social) from what the skill actually does. Otherwise pass the category through. - Secrets —
metadata.requires. An entry ending in?is optional →secrets_optional; an entry without?is required →secrets_required. Strip the?. Registrysecrets_requiredmust be UPPER_SNAKE env names. - Capabilities —
metadata.capabilities, copied verbatim (already the locked taxonomy; seedocs/CAPABILITIES.md). Omit if absent.
fm(){ sed -n '/^---$/,/^---$/p' "skills/$SLUG/SKILL.md"; } TITLE=$(fm | sed -n 's/^[[:space:]]*title:[[:space:]]*//p' | head -1) [ -z "$TITLE" ] && TITLE=$(fm | sed -n 's/^name:[[:space:]]*//p' | head -1) DESC=$(fm | sed -n 's/^description:[[:space:]]*//p' | head -1) CATEGORY=$(fm | sed -n 's/^[[:space:]]*category:[[:space:]]*//p' | head -1)Collect
requires:/capabilities:list items (lines under those keys beginning with-) into shell arrays; splitrequiresinto required (no?) and optional (trailing?, stripped). - Display name —
-
Build the pack repo locally. Stage a clean working tree — the manifest at root, the full skill directory copied verbatim (including any helper scripts/config, not just
SKILL.md), a README, and an MITLICENSE:PACK_DIR=$(mktemp -d)/pack mkdir -p "$PACK_DIR/skills/$SLUG" cp -R "skills/$SLUG/." "$PACK_DIR/skills/$SLUG/"Write
$PACK_DIR/skills-pack.json(build it withpython3/jq, never string concatenation, so quoting is safe). Include only fields you actually have:{ "name": "<TITLE>", "version": "0.1.0", "description": "<DESC>", "author": "<operator github handle>", "license": "MIT", "homepage": "https://github.com/<owner>/<pack-repo>", "skills": [ { "slug": "<SLUG>", "path": "skills/<SLUG>", "description": "<DESC>", "category": "<CATEGORY>", "schedule": "0 12 * * *", "default_enabled": false, "secrets_required": [ ... ], "secrets_optional": [ ... ], "capabilities": [ ... ] } ] }Write a
README.mdthat names the skill, states its schedule assumption, lists required/optional secrets, and shows the one-line install (bin/install-skill-pack <owner>/<pack-repo>). Write a standard MITLICENSE(year${today}'s year, copyright the operator handle). Resolve the operator handle once:OWNER=$(gh api user --jq .login). -
Pre-flight the pack. Run the repo's own validator against the staged directory — it enforces exactly what
bin/install-skill-packrequires (valid JSON manifest, clean slug, no..in paths, theSKILL.mdpresent, locked-taxonomy capabilities):./scripts/validate-pack.sh "$PACK_DIR" 2>&1 | tee /tmp/pack-validate.txtExit non-zero (an
ERROR:line) → exitPACK_SUBMIT_INVALID_PACK, notify with the failing line, and stop. Do not push an invalid pack. Warnings are fine to proceed on; surface them in the notify.If
--dry-runwas passed, stop here: notify the operator that the pack built and validated cleanly at$PACK_DIR, list the manifest fields, and exitPACK_SUBMIT_DRY_RUNwithout touching GitHub. -
Create and push the pack repo. Default repo name
aeon-skill-pack-$SLUG(or the--repooverride). The pack must be public — the installer fetches its tarball, and a private pack can't be installed by anyone else:PACK_REPO="${REPO_OVERRIDE:-aeon-skill-pack-$SLUG}" # owner defaults to $OWNER gh repo view "$PACK_REPO" >/dev/null 2>&1 \ && { ./notify "pack-submit aborted: repo $PACK_REPO already exists — pass --repo to pick another name"; exit 0; } ( cd "$PACK_DIR" && git init -q && git add -A \ && git commit -q -m "Aeon community pack: $SLUG" \ && gh repo create "$PACK_REPO" --public --source=. --push )Capture the resulting
owner/repo(FULL_REPO=$(gh repo view "$PACK_REPO" --json nameWithOwner --jq .nameWithOwner)). If repo creation fails (permission/name), exitPACK_SUBMIT_REPO_FAILED, notify with thegherror's shortest decisive line, and stop. -
Submit the registry PR against
aeonfun/aeon(skip this whole step if--no-registerwas passed - then jump to step 7 reporting only the pack repo). The registry lives in the canonical repo, so work against a fork, not this instance's checkout - this instance'scatalog/skill-packs.jsonanddocs/community-skill-packs.mdcan be stale:WORK=$(mktemp -d) gh repo fork aeonfun/aeon --clone=true --default-branch-only 2>/dev/null || true git clone -q "https://github.com/$OWNER/aeon.git" "$WORK/aeon" || git clone -q https://github.com/aeonfun/aeon.git "$WORK/aeon" cd "$WORK/aeon" && git checkout -b "pack-submit/$SLUG"Make the two edits the validator (
scripts/validate-skill-packs.mjs) checks for parity:(a)
catalog/skill-packs.json— append one object to.packs. Build it withjqfrom the values you resolved (mirror the pack's own manifest:skillsis["$SLUG"],trust_levelis"community", aggregatesecrets_required/capabilitiesfrom the skill):jq --arg repo "$FULL_REPO" --arg name "$TITLE" --arg desc "$DESC" \ --arg author "$OWNER" --arg homepage "https://github.com/$FULL_REPO" \ --arg cat "$CATEGORY" --arg slug "$SLUG" \ '.packs += [ { repo:$repo, name:$name, description:$desc, author:$author, license:"MIT", homepage:$homepage, category:$cat, trust_level:"community", skills:[$slug] } ]' catalog/skill-packs.json > /tmp/reg.json \ && mv /tmp/reg.json catalog/skill-packs.jsonAdd
secrets_required/capabilitieskeys to that object only when the skill declares them (keep it in sync with the pack manifest). Do not usetrust_level: trusted— that requires the repo to be inskills/security/trusted-sources.txt, and the validator rejects an unearnedtrusted.(b)
docs/community-skill-packs.md- add a table row under the| Pack | Skills | Description |header in the Listed packs section (the last section of the file). The table moved there from.github/README.mdin #845; do not edit the README. The row format, matching the existing rows exactly:| [<pack-repo-name>](https://github.com/<FULL_REPO>) | 1 | <one-line description, ≤110 chars>. |Use
python3for a surgical insert. The row must land inside the table's contiguous block - insert it immediately after the last existing| [row, never after a blank line or other text. The parity validator parses rows only until the first non-|line, so a row placed past the table is invisible to it and the PR fails CI with "in the registry but has no row in the Community Packs table". Verify with a re-read that the row sits among the other rows:import re lines = open("docs/community-skill-packs.md").read().split("\n") sec = next(i for i,l in enumerate(lines) if re.match(r'^#+\s+Listed packs\s*$', l)) hdr = next(i for i in range(sec,len(lines)) if re.match(r'^\|\s*Pack\s*\|\s*Skills\s*\|\s*Description\s*\|', lines[i])) i = hdr + 2; last = i while i < len(lines) and lines[i].startswith("|"): last = i; i += 1 # last data row lines.insert(last + 1, f"| [{PACK_NAME}](https://github.com/{FULL_REPO}) | 1 | {DESC} |") open("docs/community-skill-packs.md","w").write("\n".join(lines))Validate parity before committing — this is the exact CI gate the PR will hit:
node scripts/validate-skill-packs.mjsA non-zero exit → fix the reported mismatch (skill count, a missing row, or a
--pathflag) and re-run until it printsvalidate-skill-packs: OK. Never open the PR on a red validator.Then commit both files together, push the branch to your fork, and open the PR against the canonical repo:
git add catalog/skill-packs.json docs/community-skill-packs.md git commit -m "feat: list $TITLE community pack ($FULL_REPO)" git push -u origin "pack-submit/$SLUG" PR_URL=$(gh pr create --repo aeonfun/aeon --title "feat: list $TITLE community pack" --body "$(cat <<BODY ## New community pack **Pack:** [$FULL_REPO](https://github.com/$FULL_REPO) **Skill:** \`$SLUG\` — $DESC **Author:** @$OWNER · **License:** MIT · **Category:** $CATEGORY Adds a row to the Listed packs table in \`docs/community-skill-packs.md\` and a matching \`catalog/skill-packs.json\` entry (both in this diff, per the publishing checklist). \`node scripts/validate-skill-packs.mjs\` passes locally. Install once merged: \`\`\` bin/install-skill-pack $FULL_REPO \`\`\` BODY )")Capture
PR_URL. Ifgh pr createfails because a PR already exists for the branch, capture the existing URL instead of erroring. This PR is not self-merging — it lands in someone else's repo and is reviewed by the aeon maintainers. -
Log. Append to
memory/logs/${today}.md:### pack-submit - Skill: {SLUG} ({TITLE}) - Pack repo: https://github.com/{FULL_REPO} - Registry PR: {PR_URL or "skipped (--no-register)"} - Secrets: {required list or "none"} · Capabilities: {list or "none"} - Exit: PACK_SUBMIT_OK (or the code that applied) -
Notify. Send one concise line via
./notify(≤4000 chars, clickable URLs):*pack-submit — {TITLE}* Pack repo: https://github.com/{FULL_REPO} Registry PR: {PR_URL} Install: `bin/install-skill-pack {FULL_REPO}` {"Requires: X, Y" if any required secrets, else omit}On
--no-register, say the pack repo is live and give the exact PR checklist link (docs/community-skill-packs.md#pack-maintainers-publishing-checklist) so the operator can list it manually.
Exit taxonomy
| Code | When | Action |
|---|---|---|
PACK_SUBMIT_OK |
Pack repo pushed and registry PR opened (or --no-register completed) |
Notify with pack repo + PR link |
PACK_SUBMIT_NO_VAR |
${var} empty |
Notify abort reason; stop |
PACK_SUBMIT_BAD_VAR |
Slug malformed or skills/<slug>/SKILL.md missing |
Notify with the slug; stop |
PACK_SUBMIT_INVALID_PACK |
validate-pack.sh reported an ERROR |
Notify with the failing line; stop (nothing pushed) |
PACK_SUBMIT_DRY_RUN |
--dry-run — built + validated, wrote nothing to GitHub |
Notify with the staged path + manifest summary |
PACK_SUBMIT_REPO_FAILED |
gh repo create failed (name taken, permission) |
Notify with the gh error; stop |
PACK_SUBMIT_REGISTRY_FAILED |
Fork/clone/validate/PR step failed after the pack repo was pushed | Notify: pack repo is live, registry PR did not open, with the failing step |
Network note
There is no network sandbox — git and gh reach GitHub directly. gh is authenticated in Actions (via GH_GLOBAL when set; the ambient GITHUB_TOKEN otherwise, which cannot create repos or cross-repo PRs — surface that as PACK_SUBMIT_REPO_FAILED rather than looping). Every irreversible outward write — creating the public pack repo (step 5) and opening the registry PR (step 6) — happens in-run and is fail-closed: a failed pre-flight (validate-pack.sh) or a failed parity check (validate-skill-packs.mjs) stops before any push. Confirm reachability with gh api repos/aeonfun/aeon --jq .full_name before deciding a fork/clone failure is real vs transient.
Constraints
- Publish only skills that already exist in
skills/. This skill packages and distributes; it does not author. To create a new skill, usecreate-skill. - The pack repo must be public — the installer fetches its tarball; a private pack is uninstallable by others.
- Never register with
trust_level: trusted— that is earned viaskills/security/trusted-sources.txt, and the validator rejects a self-declaredtrusted. Community packs usetrust_level: community. - Never open the registry PR on a red
validate-skill-packs.mjs— a broken registry entry takes downbin/install-skill-pack --listand the dashboard panel for everyone. - Keep the two registry surfaces in lockstep - the Listed packs row in
docs/community-skill-packs.mdand theskill-packs.jsonentry ship in one diff, with matching skill counts. That is what the CI gate enforces. - Don't leak secret values. The manifest lists secret names only (from the skill's
requires:), never values.
Version History
-
531f575
Current 2026-09-27 15:14
修复 CI 验证逻辑,使其读取 docs/community-skill-packs.md 而非旧版 README;更新 pack-submit 技能以将注册行写入新文档位置并移除废弃的计数器检查。
- fc05537 2026-08-05 22:15


