- name
- aeon
- description
- Set up and run an Aeon agent instance — get started from scratch, pick which skills to turn on or install more from packs, reschedule or change what runs, edit what an existing skill does, fix a skill that isn't firing, set the STRATEGY.md north star and soul/ voice, turn a coding-agent chat into a scheduled Aeon skill, and mine past coding-agent conversations for recurring work worth automating as a skill. Use when the user mentions Aeon, aeon.yml, an Aeon skill / instance / routine / pack, asks to schedule, enable, edit, or debug an agent that runs on a cron, or asks what of their repeated/manual work Aeon could take over.
# Aeon
Aeon is an agent that runs on the user's own GitHub repo via Actions. A skill is a Markdown file (`skills/<name>/SKILL.md`); `aeon.yml` says which ones run and when.
Pick the mode they're asking for:
| | |
|---|---|
| **1 · Start** | No instance yet, or set one up from scratch |
| **2 · Reschedule** | Change times, cadence, or what a skill focuses on |
| **3 · Unblock** | "It didn't run" / "nothing happened" |
| **4 · Chat → skill** | Turn what we just did into a scheduled skill |
| **5 · Edit a skill** | Change what an existing skill does |
| **6 · What to turn on** | Pick skills, browse packs, install more |
| **7 · Strategy & voice** | `STRATEGY.md` and `soul/` — the north star and the tone |
| **8 · Mine history → skill** | "What of my repeated work could Aeon do for me?" — surface it from past coding-agent chats |
## Preflight (every mode)
1. Find the repo: current dir → `gh repo set-default` → ask. Clone it if it isn't local.
2. **Confirm `gh` points at THEIR instance, before any command that writes.**
```bash
gh repo view --json nameWithOwner -q .nameWithOwner
```
If that prints `aeonfun/aeon` and they aren't working on upstream itself, stop and run `gh repo set-default <owner>/<repo>`. `gh` prefers an `upstream` remote over `origin` when no default is pinned, and every Aeon write (`auth`, `secrets set`, `skills run`, config pushes) is a `gh -R <resolved>` call — so it will cheerfully put their API keys on the upstream repo and dispatch runs there. It looks like success: no error, a real run id, and the skill just never fires on their instance.
3. `gh auth status` — everything routes through `gh`. If it fails, tell them to run `gh auth login` and stop.
4. Use the `./aeon` CLI for all config writes. It preserves comments in `aeon.yml` and validates. Never hand-edit the YAML — with one exception: the CLI cannot *create* an entry for a brand-new skill (see Mode 4 step 4).
**Don't trust "disabled" for a skill you just created.** The read path lists skills from disk and defaults a missing `aeon.yml` entry to `enabled: false`, so "not configured" and "disabled" look identical. One command tells them apart:
```bash
comm -23 <(ls skills/*/SKILL.md | cut -d/ -f2 | sort) \
<(grep -oE '^ [a-z0-9-]+:' aeon.yml | tr -d ' :' | sort)
```
Anything it prints is on disk but unconfigured. **Orientation — what's installed, what's on, and where everything lives: `references/layout.md`.**
**Setting any key or token:** read `references/secrets.md` — it has every secret and repo variable with the exact page to get it from. Always set secrets with `./aeon secrets set NAME --stdin`, never as a command argument.
---
## Mode 1 — Start on Aeon
Goal: one real notification in their phone, fast. Do not configure a schedule first.
1. **Get a repo. Ask public or private before you run anything** — it changes the command, and switching later means moving the repo.
**Public** (recommend this): Actions minutes are free, and upstream skill updates arrive with one command.
```bash
gh repo fork aeonfun/aeon --clone && cd aeon
gh repo set-default <owner>/aeon # REQUIRED — see below
```
**Private**: a fork of a public repo is always public, so a private instance is a mirror, not a fork.
```bash
gh repo create <name> --private
git clone --bare https://github.com/aeonfun/aeon.git
git -C aeon.git push --mirror https://github.com/<owner>/<name>.git
rm -rf aeon.git && git clone https://github.com/<owner>/<name>.git && cd <name>
git remote add upstream https://github.com/aeonfun/aeon.git
gh repo set-default <owner>/<name> # REQUIRED — see below
```
Say both costs out loud before they pick private: Actions minutes bill against the account quota (2,000/mo on Free — scheduled skills burn it), and updates come from `git fetch upstream && git merge upstream/main` instead of `gh repo sync`.
**Pin the default repo before any other command — both paths.** Both end up with an `upstream` remote (`gh repo fork --clone` adds one for you), and with no default pinned **`gh` prefers `upstream` over `origin`**. Everything in Aeon routes through `gh -R $(gh repo view …)`, so an unpinned checkout silently writes secrets to and dispatches runs against `aeonfun/aeon` instead of their instance — with no error, because the commands genuinely succeed on the wrong repo. Verify:
```bash
gh repo view --json nameWithOwner -q .nameWithOwner # must print THEIR repo
```
Everything after this step is identical either way.
2. **Auth a model.** At least one is required. Fastest is `./aeon auth --oauth` (Claude Pro/Max, opens a browser), or `./aeon auth --key <key>`, which detects the provider **from the key prefix** — `sk-ant-oat` (OAuth), `sk-or-` (OpenRouter), `bk_` (Bankr), `inf_` (Surplus), `xai-` (Grok); anything else lands in `ANTHROPIC_API_KEY`.
**UsePod and Venice keys have no prefix** and are undetectable, so a bare `--key` files them as a plain Anthropic key and the run fails later with a confusing auth error. They must be named:
```bash
./aeon auth --key <token> --provider usepod # same for venice
```
`--dry-run` prints the resolved `method=… → secret …` without calling `gh` or `claude` — worth running whenever the provider is in doubt.
**Don't assume they have a Claude subscription:** nine providers work, including OpenRouter, Grok, GLM, and crypto-settled gateways. See "Providers and harnesses".
3. **Wire one channel.** Telegram is the fastest: create a bot with @BotFather, then `./aeon secrets set TELEGRAM_BOT_TOKEN --stdin` and `TELEGRAM_CHAT_ID`. Skip Discord/Slack/email for now — one channel is enough to prove it works.
4. **Run one skill now.** Pick it with Mode 6 — ask what they want handled, propose one — then `./aeon skills run <name>`. Wait for it, then `./aeon runs logs <id>`. They should get a Telegram message.
5. **Only then, schedule it.** `./aeon skills enable <name>` and set a time (see Mode 2).
Good first skills: `digest` (topic briefing), `github-monitor` (their repos), `heartbeat` (already on by default, reports only when something needs attention).
---
## Mode 2 — Reschedule / change the routine
Show them their day as a **timeline in their own timezone**, not a config file:
```
07:00 digest "solana"
09:00 pr-review your repos
18:00 heartbeat health check
```
Build it from `./aeon skills ls --enabled --json`. (`--enabled` matters: plain `ls` prints a `SCHEDULE` column for *disabled* skills too — that's their `aeon.yml` entry, not proof anything fires.) No CLI, or want the raw file? `references/layout.md` has grep-only equivalents. Then take plain-language edits and apply them:
| They say | You do |
|---|---|
| "move the digest to 7am" | `./aeon skills schedule digest "0 6 * * *"` |
| "weekdays only" | `... "0 6 * * 1-5"` |
| "too noisy, twice a week" | `... "0 6 * * 1,4"` |
| "stop the crypto one" | `./aeon skills disable token-movers` |
| "make it about rust instead" | `./aeon skills set digest --var rust` |
Rules:
- **All cron in `aeon.yml` is UTC.** Convert from their timezone, and say so: "7am Paris = `0 6 * * *` UTC (5am in summer — want it pinned to local time?" There is no local-time option, so if DST matters, tell them which half of the year is off by an hour.
- Confirm back the **next 3 fire times in their timezone** after any change.
- `--dry-run` first on anything ambiguous, show the diff, then apply.
- Changes need a push to take effect. The CLI does it; confirm it landed.
- **Then check the value came out quoted** — one grep, every time:
```bash
grep '^ <skill>:' aeon.yml
```
The scheduler only reads `schedule: "…"` **with double quotes**. The CLI writes a *new* key unquoted, so an entry that had no `schedule:` yet comes back as `schedule: 0 12 * * *` and the skill is skipped forever. Details below.
Skills with `schedule: workflow_dispatch` are on-demand only — they never fire on cron. `reactive` ones fire on conditions, not time.
---
## Mode 3 — Unblock
"It didn't run." Check in this order and stop at the first hit:
1. **Is it on?** `./aeon skills ls --enabled` — is it listed?
2. **Duplicate key?** `node scripts/validate-config.js`. A repeated skill name in `aeon.yml` silently shadows the first one. Common after hand-edits.
3. **Is it even cron?** `workflow_dispatch` and `reactive` never fire on a schedule.
4. **Are Actions disabled?** `gh api repos/{owner}/{repo}/actions/permissions`. GitHub auto-disables scheduled workflows after 60 days of repo inactivity — this silently kills forks and nothing in Aeon surfaces it. Re-enable in repo Settings.
5. **Is the schedule quoted?** `grep '^ <skill>:' aeon.yml` — the value must be `schedule: "0 12 * * *"`, **with double quotes**.
```
schedule: "0 12 * * *" ✅ fires
schedule: 0 12 * * * ❌ never fires, no error anywhere
```
`scheduler.yml` matches schedules with the bash regex `schedule: *"([^"]+)"`. An unquoted value doesn't match, `$SCHED` is empty, and the match loop hits `[ -z "$SCHED" ] && continue` — skipped silently, every tick, forever.
How it gets that way: the CLI edits `aeon.yml` through a YAML document model that preserves an *existing* quoted node but writes a **newly added** key in plain style. So `./aeon skills schedule <name> "0 12 * * *"` is safe on an entry that already had a quoted `schedule:`, and quietly breaks one that didn't. Same for a first-time `--var`.
**Nothing else detects this.** The file is valid YAML, `validate-config.js` reports CLEAN, and `./aeon skills ls --enabled` lists the skill with its schedule — because they all parse YAML properly and only the scheduler uses a regex. Fix by adding the quotes by hand.
6. **Did it run and fail?** `./aeon runs ls` then `./aeon runs logs <id>`. A failed skill retries after a 30-minute cooldown.
Three more, if the above are clean:
- **It ran against the wrong repo.** The giveaway is a command that reported success with a run id, but `./aeon runs ls` on their instance shows nothing. `gh` prefers `upstream` over `origin` when no default is pinned, so an unpinned checkout sends every write to `aeonfun/aeon`.
```bash
gh repo view --json nameWithOwner -q .nameWithOwner # if this isn't their repo:
gh repo set-default <owner>/<repo>
```
Then **clean up what landed upstream** — re-running against the right repo does not undo it. Any key set while mispointed is now a secret on someone else's repo:
```bash
gh secret list -R aeonfun/aeon # timestamps matching the misfire = theirs
```
**Rotate it at the provider first, always** — it sat on a repo whose collaborators can land a workflow that reads it. Then re-set it on their instance with `./aeon secrets set NAME --stdin`.
**Don't blind-delete it.** `gh secret list` shows only *last-updated*, so it cannot tell you whether the upstream repo already had that secret and the misfire **overwrote** it. Ask before removing:
- Upstream never had it → `gh secret delete <NAME> -R <upstream>`.
- Upstream had its own → deleting breaks *their* scheduled runs. The owner must re-set upstream's own value; the overwrite is not reversible from here.
If the delete 403s, they never had write access — nothing was ever written, and the earlier command failed while only *looking* fine.
- **Missing secret.** Skills declare keys in `requires:`. Check them against `./aeon secrets ls --set`. A missing optional key (`KEY?`) means it degrades quietly, not that it breaks.
- **"No MCP tools available."** On the Claude harness a single unresolved `${VAR}` in `.mcp.json` disables **every** MCP server for that run, not just the broken one (`::warning::.mcp.json references secret(s) not set:` … `Skipping MCP this run.`). Grok degrades per-server instead. If an OAuth server broke a run *after* working, suspect a rotated refresh token that couldn't be saved — `references/mcp.md`.
- **It ran but sent nothing.** That's usually correct. Aeon's convention is silence on no signal — a clean run sends nothing rather than an empty report.
Note: GitHub only delivers ~10% of `*/5` cron ticks, so the scheduler catches up missed slots for up to 12 hours. A skill firing 40 minutes late is normal.
---
## Mode 4 — Turn this chat into a skill
They just did something in this chat and want it to happen on a schedule.
1. **Write the skill file.** `skills/<name>/SKILL.md` — frontmatter, then the prompt. Derive it from what actually happened in the session:
- the prompt body = what they asked for, plus the steps that worked
- `mode:` = `read-only` unless it needs to commit or open PRs
- `requires:` = any API key the work hit (`KEY?` if it can degrade without it)
- `category:` = one of `core evolution basics dev crypto productivity`
- if they liked the output, paste a trimmed sample into the body as the format spec
2. **Fix the three things that break unattended runs:**
- **Nobody's there.** Any point where you asked them a question has to become a default or a rule.
- **Stay silent on nothing.** Add an explicit "if there's nothing worth reporting, log and exit without notifying." Otherwise it gets muted in a week.
- **Don't repeat yesterday.** Add "check the last 3 days of `memory/logs/` and skip anything already reported."
3. **Check it can actually run there.** No local filesystem, no logged-in tools. If the session read their home directory or used a local MCP server, say so plainly — that part won't work unattended unless it's wired as a repo secret / `.mcp.json`. Wiring an MCP server for unattended use (dashboard Connect, OAuth refresh, the rotating-token PAT): `references/mcp.md`.
4. **Add the `aeon.yml` entry yourself.** A new skill on disk has no entry, and `./aeon skills enable|schedule` **will not create one** — they only flip entries that already exist, and report `no change — already in that state`, which is false. Add it by hand, disabled, before the fallback `heartbeat:` line:
```yaml
my-skill: { enabled: false, schedule: "0 12 * * *" }
```
**Include the quoted `schedule:` even though it's disabled — the quotes are load-bearing.** Writing a bare `{ enabled: false }` and letting `./aeon skills schedule` add the key later produces an *unquoted* value the scheduler cannot read, and the skill never fires (Mode 3, check 5). Seeding a quoted node here means every later CLI edit preserves the quotes.
Match the inline `{ … }` form the other 61 entries use, on one line. `aeon.yml:367` reads per-skill `model:`/`harness:` overrides with a single-line grep, so an entry split across lines takes the global default instead.
This is the one sanctioned exception to "never hand-edit the YAML". Validate after: `node scripts/validate-config.js` — but note it only checks structure, and will not catch an unquoted value.
5. **Regenerate BOTH catalogs, then ship it as a PR.** A new skill trips three CI gates. Run them locally — **nothing blocks a merge on red**, `main` is unprotected and has no rulesets, so an unrun gate just fails after the fact:
```bash
bash scripts/check-skill-categories.sh # category is one of the six
bin/generate-skills-json # catalog/skills.json
bin/generate-packs-json # catalog/packs.json — NOT optional
```
`generate-packs-json` is the one everyone forgets: `catalog/skills.json` is itself a trigger path for `ci-packs-json`, so committing the skills catalog without the pack catalog goes red on a workflow you never touched. Commit both files.
Full gate list, triggers, and the `ci-tests` / `ci-apps` commands: `references/ci.md`.
6. **Run it once** (`./aeon skills run <name>`), show them the output, then schedule it via Mode 2.
### Skill file shape
```yaml
---
name: my-skill
description: One line — what it does and what it sends.
metadata:
title: My Skill
mode: read-only
category: basics
var: ""
tags:
- content
requires:
- SOME_API_KEY?
---
Today is ${today}. <the prompt — plain instructions, including judgment calls>
## Steps
1. <the procedure - 44 of 81 skills lead with this>
## Network note
<curl / WebFetch / `./secretcurl` / `gh api` — how this skill fetches>
## Log
Ver en GitHub