aeonfun / pack-submit
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:`bash
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
fi`
If validation fails, exitPACK_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:`bash
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:`json
{
"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 aREADME.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):`bash
./scripts/validate-pack.sh "$PACK_DIR" 2>&1 | tee /tmp/pack-validate.txt`
Exit non-zero (anERROR: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:`bash
PACKREPO="${REPOOVERRIDE:-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 resultingowner/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:`bash
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):`bash
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.json`
Addsecrets_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> | 1 | <one-line description, ≤110 chars>. |`
Usepython3for 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:`python
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'^|\sPack\s|\sSkills\s|\sDescription\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} | 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:
`bash
node scripts/validate-skill-packs.mjs`
A 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:
`bash
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 packPack: $FULL_REPO
Skill: \$SLUG\— $DESC
Author: @$OWNER · License: MIT · Category: $CATEGORYAdds 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
)")`
CapturePR_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: PACKSUBMITOK (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.