- 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
Auf GitHub ansehen