Installs into .claude/skills of the current project.
Are you the author of Pack Submit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/aeonfun-pack-submit)
---
name: pack-submit
description: Package one of this agent's own skills as a standalone community pack and submit it to the aeon registry as a PR
metadata:
title: Pack Submit
category: evolution
var: ""
tags:
- dev
- meta
- packs
mode: write
requires:
- GH_GLOBAL?
capabilities:
- external_api
- writes_external_host
- sends_notifications
---
> **${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` — package `skills/token-movers/` into a fresh public repo and submit it to the aeon registry
> - `token-movers --repo myorg/aeon-token-movers` — override the pack repo name (default `aeon-skill-pack-<slug>`)
> - `token-movers --no-register` — create and push the pack repo, but skip the registry PR against `aeonfun/aeon`
> - `token-movers --dry-run` — build and validate the pack locally, write nothing to GitHub
If `${var}` is empty, exit `PACK_SUBMIT_NO_VAR`:
```bash
./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
1. **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, exit `PACK_SUBMIT_BAD_VAR` with the notify above and stop. Extract the boolean flags from `$FLAGS` and the optional `--repo` value.
2. **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-level `name:`.
- **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 `?`. Registry `secrets_required` must be UPPER_SNAKE env names.
- **Capabilities** — `metadata.capabilities`, copied verbatim (already the locked taxonomy; see `docs/CAPABILITIES.md`). Omit if absent.
```bash
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; split `requires` into required (no `?`) and optional (trailing `?`, stripped).
3. **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 MIT `LICENSE`:
```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 with `python3`/`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 a `README.md` that 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 MIT `LICENSE` (year `${today}`'s year, copyright the operator handle). Resolve the operator handle once: `OWNER=$(gh api user --jq .login)`.
4. **Pre-flight the pack.** Run the repo's own validator against the staged directory — it enforces exactly what `bin/install-skill-pack` requires (valid JSON manifest, clean slug, no `..` in paths, the `SKILL.md` present, locked-taxonomy capabilities):
```bash
./scripts/validate-pack.sh "$PACK_DIR" 2>&1 | tee /tmp/pack-validate.txt
```
Exit non-zero (an `ERROR:` line) → exit `PACK_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-run` was passed, stop here: notify the operator that the pack built and validated cleanly at `$PACK_DIR`, list the manifest fields, and exit `PACK_SUBMIT_DRY_RUN` without touching GitHub.
5. **Create and push the pack repo.** Default repo name `aeon-skill-pack-$SLUG` (or the `--repo` override). The pack must be **public** — the installer fetches its tarball, and a private pack can't be installed by anyone else:
```bash
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), exit `PACK_SUBMIT_REPO_FAILED`, notify with the `gh` error's shortest decisive line, and stop.
6. **Submit the registry PR against `aeonfun/aeon`** (skip this whole step if `--no-register` was 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's `catalog/skill-packs.json` and `docs/community-skill-packs.md` can 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 with `jq` from the values you resolved (mirror the pack's own manifest: `skills` is `["$SLUG"]`, `trust_level` is `"community"`, aggregate `secrets_required`/`capabilities` from 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
```
Add `secrets_required` / `capabilities` keys to that object only when the skill declares them (keep it in sync with the pack manifest). Do **not** use `trust_level: trusted` — that requires the repo to be in `skills/security/trusted-sources.txt`, and the validator rejects an unearned `trusted`.
**(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.md` in #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 `python3` for 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'^\|\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:
```bash
node scripts/validate-skill-packs.mjs
```
A non-zero exit → fix the reported mismatch (skill count, a missing row, or a `--path` flag) and re-run until it prints `validate-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 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`. If `gh pr create` fails 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.
7. **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)
```
8. **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, use `create-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 via `skills/security/trusted-sources.txt`, and the validator rejects a self-declared `trusted`. Community packs use `trust_level: community`.
- **Never** open the registry PR on a red `validate-skill-packs.mjs` — a broken registry entry takes down `bin/install-skill-pack --list` and the dashboard panel for everyone.
- **Keep the two registry surfaces in lockstep** - the Listed packs row in `docs/community-skill-packs.md` and the `skill-packs.json` entry 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.