---
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.
