[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CZ4JSYJ1EYK4C750R2EWD2":3,"skill-same:01M6CZ4JSYJ1EYK4C750R2EWD2":77},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":54,"commitSha":55,"skillDirectory":56,"forksCount":57,"repositoryHtmlUrl":58,"repositoryHomepageUrl":59,"ownerHtmlUrl":60,"ownerBlogUrl":59,"ownerDescription":61,"updatedAt":62,"install":63,"resources":76},"01M6CZ4JSYJ1EYK4C750R2EWD2","aeon-doctor",null,"Static config-correctness linter for this instance - catches the silent-failure class (unquoted schedules, duplicate keys, unconfigured skills, mode typos, broken requires\u002FMCP refs) that no run-based health skill can see. Notifies only on problems.","aeonfun\u002Faeon","aeonfun","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F287715280?v=4","01M6CZ23C6MXGMF4N0V1DZ1247","aeon","01M6CZ23GMZP1BR424J8Y8K3DG",793,"MIT","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Faeon-doctor\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[24,12,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"doctor","aeon doctor","evolution","schedules","static","for","mode","can","typos","correctness","read-only","only","MCP","read","linter","broken","health","Meta","run","config","2026-11-10 10:12:30",false,"---\nname: aeon-doctor\ndescription: Static config-correctness linter for this instance - catches the silent-failure class (unquoted schedules, duplicate keys, unconfigured skills, mode typos, broken requires\u002FMCP refs) that no run-based health skill can see. Notifies only on problems.\nmetadata:\n  title: Aeon Doctor\n  category: evolution\n  var: \"\"  # \"\"=lint the whole instance config | \u003Cskill-slug>=lint one skill's entry + SKILL.md\n  tags:\n    - meta\n    - health\n  mode: read-only\n---\n\n> **${var}** — scope. **Empty (default)** = lint the entire instance config (`aeon.yml` + every `skills\u002F*\u002FSKILL.md` + `.mcp.json`). A **skill slug** (e.g. `digest`) = lint just that one skill's `aeon.yml` entry and its `SKILL.md`.\n\nToday is ${today}. You are this instance's **config doctor**. Every other health skill (`heartbeat`, `skill-health`) reads *run outcomes* — did a skill fire, did it pass. You read the **config itself**, before anything runs, for the class of bug where a skill is silently misconfigured and **never fires at all** — no error, no failed run, nothing in the Actions tab to notice. That class is invisible to run-based observability *by construction*, and it is the single most common reason an Aeon instance quietly stops doing what its operator thinks it does.\n\nYou do **not** fix anything — a diagnostic that inspects config must never mutate it. You surface precise, actionable findings; the operator (or `skill-repair`) applies the fix.\n\n## Preamble (always)\n\n1. Read `memory\u002FMEMORY.md` for context and scan the last ~3 days of `memory\u002Flogs\u002F` — **drop any finding you already reported** so you don't re-nag a known-but-unfixed issue every run. (A finding is \"the same\" if it's the same check on the same skill.)\n2. Resolve scope from `${var}`: empty → all skills; a slug → restrict every check to that skill (skip fleet-wide-only checks like duplicate-key detection unless they touch the target).\n3. Every check below is a **pure local file read** - `grep`, `sort`, and `node` (inline or `node scripts\u002F*.js`). This skill is `read-only`, whose tool allowlist (`scripts\u002Fskill_mode.sh`) has `grep`\u002F`sort`\u002F`head`\u002F`tail`\u002F`wc`\u002F`cat`\u002F`jq`\u002F`node` but not `awk`\u002F`sed`\u002F`comm`\u002F`bash`, so the checks use those. No network, no secrets, no GitHub API. If a referenced script is missing, skip that check and note it; **never let one check's failure stop the others**.\n\n## Steps — run every check, collect findings\n\nEach finding = **{check, skill, severity, one-line what's-wrong, exact fix}**. Severity:\n- **critical** — an `enabled: true` skill that will **never fire** or will run with the **wrong privilege**. Live breakage.\n- **warn** — a latent trap: the same defect on a *disabled* skill, or a correctness issue that degrades silently rather than killing the run.\n\n### 1 · Unquoted `schedule:` on the old regex scheduler (critical \u002F warn)\nApplies only when `scripts\u002Fparse-aeon-config.sh` is **missing**. With it, `scheduler.yml` reads `aeon.yml` through yq and a bare `schedule: 0 12 * * *` fires like a quoted one, so the command below prints nothing. Without it, `scheduler.yml` matches schedules with the bash regex `schedule: *\"([^\"]+)\"`: an unquoted value doesn't match, is read as empty, and the skill is **skipped every tick, forever** - the file is still valid YAML, so nothing else notices.\n```bash\n[ -f scripts\u002Fparse-aeon-config.sh ] || grep -nE '^\\s+[a-z0-9-]+:\\s*\\{[^}]*schedule:' aeon.yml | grep -vE 'schedule: *\"'\n```\nEach printed line is an entry whose `schedule:` isn't double-quoted. **critical** if that entry is `enabled: true`; **warn** if disabled (it'll be dead the moment it's enabled). Fix: add the quotes (`schedule: \"0 12 * * *\"`), or pull upstream to get the yq scheduler.\n\n### 2 · Duplicate skill keys — silent shadow (critical)\nA repeated skill name under the `skills:` map silently disables the first copy (last-wins YAML).\n```bash\nnode scripts\u002Fvalidate-config.js                        # authoritative — dup keys, misspelled entry keys, checkout ordering\nnode -e 'const k=require(\"fs\").readFileSync(\"aeon.yml\",\"utf8\").match(\u002F^  [a-z0-9-]+:\u002Fgm)||[];const c={};for(const x of k)c[x]=(c[x]||0)+1;for(const x in c)if(c[x]>1)console.log(x.trim().slice(0,-1))'    # names appearing more than once\n```\nAny printed name (or a dup-key error from the validator) is a finding. **critical** if either copy is enabled. Fix: remove the shadow copy.\n\n### 3 · On disk but unconfigured — invisible skills (warn)\nA skill with a `SKILL.md` but no `aeon.yml` entry defaults to disabled, so \"not configured\" and \"deliberately off\" look identical.\n```bash\nnode -e 'const fs=require(\"fs\");const k=new Set((fs.readFileSync(\"aeon.yml\",\"utf8\").match(\u002F^  [a-z0-9-]+:\u002Fgm)||[]).map(x=>x.trim().slice(0,-1)));for(const d of fs.readdirSync(\"skills\").sort())if(fs.existsSync(\"skills\u002F\"+d+\"\u002FSKILL.md\")&&!k.has(d))console.log(d)'\n```\nEach printed name exists on disk but has no config entry. **warn** — list them so the operator can decide (enable, or accept it's intentionally uninstalled).\n\n### 4 · Enabled skill with no `SKILL.md` — broken entry (critical)\nThe inverse: an `aeon.yml` entry pointing at a skill dir that doesn't exist. For every `enabled: true` key, confirm `skills\u002F\u003Ckey>\u002FSKILL.md` is present. Missing → **critical** (the run fails or no-ops).\n\n### 5 · `requires:` entry the allowlist silently drops (warn)\nBoth list forms parse — inline (`requires: [KEY?]`) and block (`- KEY` on its own line), top-level or nested under `metadata:`. What still bites is the *value*: `scripts\u002Fskill_requires.sh` injects only names matching `^[A-Z][A-Z0-9_]{2,}$` (a trailing `?` = \"works better with\" is allowed). An entry that fails the filter — lowercase, fewer than 3 chars, a leading digit, or stray punctuation — is silently dropped, so the skill declares a credential it never receives and fails or degrades with a confusing auth error.\n```bash\nnode -e 'const fs=require(\"fs\");for(const d of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+d+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const fm=(fs.readFileSync(p,\"utf8\").split(\u002F^---$\u002Fm)[1]||\"\").split(\"\\n\");fm.forEach((l,i)=>{const m=l.match(\u002F^\\s*requires:\\s*(.*)$\u002F);if(!m)return;let it=[];if(m[1].includes(\"[\"))it=m[1].replace(\u002F.*\\[\u002F,\"\").replace(\u002F\\].*\u002F,\"\").split(\",\");else for(let j=i+1;j\u003Cfm.length&&\u002F^\\s*-\\s*\u002F.test(fm[j]);j++)it.push(fm[j].replace(\u002F^\\s*-\\s*\u002F,\"\").replace(\u002F\\s*#.*\u002F,\"\"));for(let x of it){x=x.replace(\u002F\\s\u002Fg,\"\").replace(\u002F\\?$\u002F,\"\");if(x&&!\u002F^[A-Z][A-Z0-9_]{2,}$\u002F.test(x))console.log(p+\": requires entry \\\"\"+x+\"\\\" is dropped by the allowlist filter\")}})}'\n```\nFlag any entry that fails the filter. **warn**. Fix: use the exact env-var name (uppercase, `^[A-Z][A-Z0-9_]{2,}$`), with a trailing `?` only to mark it optional.\n\n### 6 · `mode:` typo — silent write grant (critical)\nAn unknown `mode:` value falls back to **`write`**, never to the safer tier. The only valid strings are `read-only` and `write`.\n```bash\ngrep -rnE '^[[:space:]]*mode:' skills\u002F*\u002FSKILL.md | grep -vE ':\\s*(read-only|write)\\s*$'\n```\n`mode:` is nested under `metadata:` (spec form), so the pattern allows leading indent. Any printed line is a typo (`readonly`, `read only`, `readOnly`, …) that silently grants full write \u002F `gh` \u002F `git`. **critical** — least-privilege is broken. Fix: the exact string `read-only`. (A skill with *no* `mode:` line is intentionally `write` — not a finding.)\n\n### 7 · `.mcp.json` unresolved `${VAR}` — kills ALL MCP (warn)\nOn the Claude harness a **single** `${VAR}` in `.mcp.json` that resolves to no secret disables **every** MCP server that run (`Skipping MCP this run.`), not just the broken one. List the referenced vars so the operator can confirm each is set:\n```bash\ngrep -oE '\\$\\{[A-Z0-9_]+\\}' .mcp.json | sort -u    # no .mcp.json = no output\n```\nReport the list as **warn** with the note: verify each with `.\u002Faeon secrets ls --set`; any one unset silently blacks out MCP for skills that rely on it. (You can't read secret values in read-only mode — surface the vars, don't try to resolve them.)\n\n### 8 · Multi-line `aeon.yml` entry — override ignored (warn)\nPer-skill `model:` \u002F `harness:` overrides are read by a single-line grep. An entry split across lines (its `{` and `}` on different lines) takes the **global default** instead — no error, and the run's `model=` line looks normal.\n```bash\ngrep -nE '^\\s+[a-z0-9-]+:\\s*\\{[^}]*$' aeon.yml    # opens { with no closing } on the same line\n```\nEach hit is a split entry → **warn**. Fix: collapse it to one inline `{ … }` line.\n\n### 9 · Misleading `schedule:` \u002F `cron:` in SKILL.md frontmatter (warn)\nSchedules live in `aeon.yml` only; `scheduler.yml` never reads `SKILL.md`. A `schedule:` \u002F `cron:` line in frontmatter looks load-bearing but does nothing — a trap for anyone editing the skill.\n```bash\ngrep -rnE '^[[:space:]]*(schedule|cron):' skills\u002F*\u002FSKILL.md\n```\nReport as **warn** (informational): these lines are inert; the real schedule is the `aeon.yml` entry (the pattern allows leading indent, since spec-form frontmatter nests these under `metadata:`).\n\n### 10 · Unquoted per-skill `harness:` \u002F `model:` override (warn)\nThe override read (`scripts\u002Fskill_entry.sh` plus a quoted-value match) requires double quotes, even where the yq scheduler accepts a bare `schedule:`. An unquoted `harness: grok` or `model: …` is silently ignored and the skill keeps running the global default.\n```bash\ngrep -nE '\\{[^}]*(harness|model):' aeon.yml | grep -vE '(harness|model): *\"'\n```\nEach printed line has an unquoted override → **warn**. Fix: quote it (`harness: \"grok\"`).\n\n### 11 · Category not one of the six — trips CI (warn)\nEvery skill's `category:` must be one of `core evolution basics dev crypto productivity` or the catalog CI gate goes red.\n```bash\nnode -e 'const fs=require(\"fs\");const V=[\"core\",\"evolution\",\"basics\",\"dev\",\"crypto\",\"productivity\"];for(const d of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+d+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const m=(fs.readFileSync(p,\"utf8\").split(\u002F^---$\u002Fm)[1]||\"\").match(\u002F^\\s*category:\\s*[\"\\x27]?([A-Za-z-]+)\u002Fm);if(!m||!V.includes(m[1]))console.log(d+\": category \"+(m?m[1]:\"(missing)\")+\" is not one of \"+V.join(\" \"))}'    # same set as scripts\u002Fcheck-skill-categories.sh\n```\nReport any offender as **warn**.\n\n### 12 · Daily-log heading not `### \u003Cslug>` — health loop can't key it (warn)\n`CLAUDE.md` mandates each skill append its daily-log entry under a **`### \u003Cslug>`** heading — \"the health loop parses this shape\", and `skill-health` \u002F `heartbeat` key skills by **slug**. A skill that logs under `## \u003CDisplay Name>` (wrong level *and* wrong identifier) still runs, but its narrative log is harder for the health view to attribute and for the cross-skill dedup (\"read the last 3 days of logs\") to match — a silent degrade, never an error.\n```bash\nnode -e 'const fs=require(\"fs\");const e=x=>x.replace(\u002F[.*+?^${}()|[\\]\\\\]\u002Fg,\"\\\\$&\");for(const s of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+s+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const t=fs.readFileSync(p,\"utf8\");if(!\u002Fmemory\\\u002Flogs\\\u002F\\$\\{today\\}\u002F.test(t))continue;if(new RegExp(\"###\\\\s+\"+e(s)+\"\\\\b\").test(t))continue;const n=((t.match(\u002F^name:\\s*(.*)$\u002Fm)||[])[1]||s).trim();const h=t.match(new RegExp(\"^##\\\\s+(\"+e(s)+\"|\"+e(n)+\")\\\\b\",\"m\"));if(h)console.log(s+\" logs under \\\"\"+h[0]+\"\\\" - should be \\\"### \"+s+\"\\\"\")}'\n```\nEach hit → **warn**. Fix: change the Log-section heading (the instruction line *and* the example block) to `### \u003Cslug>`, and demote any sub-sections inside the block to `####`.\n\n## Report\n\n- **No findings → send nothing and exit.** A clean config is the common case; silence is correct and keeps this channel trustworthy.\n- **Findings → one consolidated `.\u002Fnotify`**, most-severe first. Write the body to a scratch file and send with `-f` (never a long argv):\n  ```bash\n  .\u002Fnotify -f \u003Cfile> \\\n    --title \"aeon-doctor: \u003CN> config issue(s)\" \\\n    --severity \u003Ccritical if any critical else warn> \\\n    --mute-key \"aeon-doctor\"\n  ```\n  Group by severity. For each finding give: the skill, one line on what breaks (and that it's **silent** — the operator won't see it in the Actions tab), and the exact one-command fix. If a `critical` exists, lead with it — an enabled skill that never fires is the whole reason this skill exists.\n- Do **not** open a PR or edit any file. Point mechanical fixes at `skill-repair` \u002F the `.\u002Faeon` CLI; leave the fix to the operator.\n\n## Constraints\n\n- Read-only by contract — inspect config, never mutate it. No `Write` \u002F `Edit` \u002F `git` \u002F `gh`.\n- Every finding must cite the **exact** file + line and a **copy-pasteable** fix. A config finding with no fix is noise.\n- Don't invent problems: only report what a check actually matched. If every check is clean, say nothing.\n- Fully local — no network, no secrets, no GitHub API. Run-outcome health is `skill-health`'s job; live attention is `heartbeat`'s. Stay in your lane: the **static config**.\n\n## Log\n\nThis skill is `read-only`, so the workflow's read-only guard writes its `### aeon-doctor` log entry from your captured output; a self-written entry would be a duplicate. Don't append to `memory\u002Flogs\u002F` yourself - put this record in your **final output** as bullets: checks run, findings by severity (or `clean`), and whether a notification was sent.\nEnd-states: `AEON_DOCTOR_CLEAN`, `AEON_DOCTOR_FINDINGS`, `AEON_DOCTOR_ERROR`.\n","> **${var}** — scope. **Empty (default)** = lint the entire instance config (`aeon.yml` + every `skills\u002F*\u002FSKILL.md` + `.mcp.json`). A **skill slug** (e.g. `digest`) = lint just that one skill's `aeon.yml` entry and its `SKILL.md`.\n\nToday is ${today}. You are this instance's **config doctor**. Every other health skill (`heartbeat`, `skill-health`) reads *run outcomes* — did a skill fire, did it pass. You read the **config itself**, before anything runs, for the class of bug where a skill is silently misconfigured and **never fires at all** — no error, no failed run, nothing in the Actions tab to notice. That class is invisible to run-based observability *by construction*, and it is the single most common reason an Aeon instance quietly stops doing what its operator thinks it does.\n\nYou do **not** fix anything — a diagnostic that inspects config must never mutate it. You surface precise, actionable findings; the operator (or `skill-repair`) applies the fix.\n\n## Preamble (always)\n\n1. Read `memory\u002FMEMORY.md` for context and scan the last ~3 days of `memory\u002Flogs\u002F` — **drop any finding you already reported** so you don't re-nag a known-but-unfixed issue every run. (A finding is \"the same\" if it's the same check on the same skill.)\n2. Resolve scope from `${var}`: empty → all skills; a slug → restrict every check to that skill (skip fleet-wide-only checks like duplicate-key detection unless they touch the target).\n3. Every check below is a **pure local file read** - `grep`, `sort`, and `node` (inline or `node scripts\u002F*.js`). This skill is `read-only`, whose tool allowlist (`scripts\u002Fskill_mode.sh`) has `grep`\u002F`sort`\u002F`head`\u002F`tail`\u002F`wc`\u002F`cat`\u002F`jq`\u002F`node` but not `awk`\u002F`sed`\u002F`comm`\u002F`bash`, so the checks use those. No network, no secrets, no GitHub API. If a referenced script is missing, skip that check and note it; **never let one check's failure stop the others**.\n\n## Steps — run every check, collect findings\n\nEach finding = **{check, skill, severity, one-line what's-wrong, exact fix}**. Severity:\n- **critical** — an `enabled: true` skill that will **never fire** or will run with the **wrong privilege**. Live breakage.\n- **warn** — a latent trap: the same defect on a *disabled* skill, or a correctness issue that degrades silently rather than killing the run.\n\n### 1 · Unquoted `schedule:` on the old regex scheduler (critical \u002F warn)\nApplies only when `scripts\u002Fparse-aeon-config.sh` is **missing**. With it, `scheduler.yml` reads `aeon.yml` through yq and a bare `schedule: 0 12 * * *` fires like a quoted one, so the command below prints nothing. Without it, `scheduler.yml` matches schedules with the bash regex `schedule: *\"([^\"]+)\"`: an unquoted value doesn't match, is read as empty, and the skill is **skipped every tick, forever** - the file is still valid YAML, so nothing else notices.\n```bash\n[ -f scripts\u002Fparse-aeon-config.sh ] || grep -nE '^\\s+[a-z0-9-]+:\\s*\\{[^}]*schedule:' aeon.yml | grep -vE 'schedule: *\"'\n```\nEach printed line is an entry whose `schedule:` isn't double-quoted. **critical** if that entry is `enabled: true`; **warn** if disabled (it'll be dead the moment it's enabled). Fix: add the quotes (`schedule: \"0 12 * * *\"`), or pull upstream to get the yq scheduler.\n\n### 2 · Duplicate skill keys — silent shadow (critical)\nA repeated skill name under the `skills:` map silently disables the first copy (last-wins YAML).\n```bash\nnode scripts\u002Fvalidate-config.js                        # authoritative — dup keys, misspelled entry keys, checkout ordering\nnode -e 'const k=require(\"fs\").readFileSync(\"aeon.yml\",\"utf8\").match(\u002F^  [a-z0-9-]+:\u002Fgm)||[];const c={};for(const x of k)c[x]=(c[x]||0)+1;for(const x in c)if(c[x]>1)console.log(x.trim().slice(0,-1))'    # names appearing more than once\n```\nAny printed name (or a dup-key error from the validator) is a finding. **critical** if either copy is enabled. Fix: remove the shadow copy.\n\n### 3 · On disk but unconfigured — invisible skills (warn)\nA skill with a `SKILL.md` but no `aeon.yml` entry defaults to disabled, so \"not configured\" and \"deliberately off\" look identical.\n```bash\nnode -e 'const fs=require(\"fs\");const k=new Set((fs.readFileSync(\"aeon.yml\",\"utf8\").match(\u002F^  [a-z0-9-]+:\u002Fgm)||[]).map(x=>x.trim().slice(0,-1)));for(const d of fs.readdirSync(\"skills\").sort())if(fs.existsSync(\"skills\u002F\"+d+\"\u002FSKILL.md\")&&!k.has(d))console.log(d)'\n```\nEach printed name exists on disk but has no config entry. **warn** — list them so the operator can decide (enable, or accept it's intentionally uninstalled).\n\n### 4 · Enabled skill with no `SKILL.md` — broken entry (critical)\nThe inverse: an `aeon.yml` entry pointing at a skill dir that doesn't exist. For every `enabled: true` key, confirm `skills\u002F\u003Ckey>\u002FSKILL.md` is present. Missing → **critical** (the run fails or no-ops).\n\n### 5 · `requires:` entry the allowlist silently drops (warn)\nBoth list forms parse — inline (`requires: [KEY?]`) and block (`- KEY` on its own line), top-level or nested under `metadata:`. What still bites is the *value*: `scripts\u002Fskill_requires.sh` injects only names matching `^[A-Z][A-Z0-9_]{2,}$` (a trailing `?` = \"works better with\" is allowed). An entry that fails the filter — lowercase, fewer than 3 chars, a leading digit, or stray punctuation — is silently dropped, so the skill declares a credential it never receives and fails or degrades with a confusing auth error.\n```bash\nnode -e 'const fs=require(\"fs\");for(const d of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+d+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const fm=(fs.readFileSync(p,\"utf8\").split(\u002F^---$\u002Fm)[1]||\"\").split(\"\\n\");fm.forEach((l,i)=>{const m=l.match(\u002F^\\s*requires:\\s*(.*)$\u002F);if(!m)return;let it=[];if(m[1].includes(\"[\"))it=m[1].replace(\u002F.*\\[\u002F,\"\").replace(\u002F\\].*\u002F,\"\").split(\",\");else for(let j=i+1;j\u003Cfm.length&&\u002F^\\s*-\\s*\u002F.test(fm[j]);j++)it.push(fm[j].replace(\u002F^\\s*-\\s*\u002F,\"\").replace(\u002F\\s*#.*\u002F,\"\"));for(let x of it){x=x.replace(\u002F\\s\u002Fg,\"\").replace(\u002F\\?$\u002F,\"\");if(x&&!\u002F^[A-Z][A-Z0-9_]{2,}$\u002F.test(x))console.log(p+\": requires entry \\\"\"+x+\"\\\" is dropped by the allowlist filter\")}})}'\n```\nFlag any entry that fails the filter. **warn**. Fix: use the exact env-var name (uppercase, `^[A-Z][A-Z0-9_]{2,}$`), with a trailing `?` only to mark it optional.\n\n### 6 · `mode:` typo — silent write grant (critical)\nAn unknown `mode:` value falls back to **`write`**, never to the safer tier. The only valid strings are `read-only` and `write`.\n```bash\ngrep -rnE '^[[:space:]]*mode:' skills\u002F*\u002FSKILL.md | grep -vE ':\\s*(read-only|write)\\s*$'\n```\n`mode:` is nested under `metadata:` (spec form), so the pattern allows leading indent. Any printed line is a typo (`readonly`, `read only`, `readOnly`, …) that silently grants full write \u002F `gh` \u002F `git`. **critical** — least-privilege is broken. Fix: the exact string `read-only`. (A skill with *no* `mode:` line is intentionally `write` — not a finding.)\n\n### 7 · `.mcp.json` unresolved `${VAR}` — kills ALL MCP (warn)\nOn the Claude harness a **single** `${VAR}` in `.mcp.json` that resolves to no secret disables **every** MCP server that run (`Skipping MCP this run.`), not just the broken one. List the referenced vars so the operator can confirm each is set:\n```bash\ngrep -oE '\\$\\{[A-Z0-9_]+\\}' .mcp.json | sort -u    # no .mcp.json = no output\n```\nReport the list as **warn** with the note: verify each with `.\u002Faeon secrets ls --set`; any one unset silently blacks out MCP for skills that rely on it. (You can't read secret values in read-only mode — surface the vars, don't try to resolve them.)\n\n### 8 · Multi-line `aeon.yml` entry — override ignored (warn)\nPer-skill `model:` \u002F `harness:` overrides are read by a single-line grep. An entry split across lines (its `{` and `}` on different lines) takes the **global default** instead — no error, and the run's `model=` line looks normal.\n```bash\ngrep -nE '^\\s+[a-z0-9-]+:\\s*\\{[^}]*$' aeon.yml    # opens { with no closing } on the same line\n```\nEach hit is a split entry → **warn**. Fix: collapse it to one inline `{ … }` line.\n\n### 9 · Misleading `schedule:` \u002F `cron:` in SKILL.md frontmatter (warn)\nSchedules live in `aeon.yml` only; `scheduler.yml` never reads `SKILL.md`. A `schedule:` \u002F `cron:` line in frontmatter looks load-bearing but does nothing — a trap for anyone editing the skill.\n```bash\ngrep -rnE '^[[:space:]]*(schedule|cron):' skills\u002F*\u002FSKILL.md\n```\nReport as **warn** (informational): these lines are inert; the real schedule is the `aeon.yml` entry (the pattern allows leading indent, since spec-form frontmatter nests these under `metadata:`).\n\n### 10 · Unquoted per-skill `harness:` \u002F `model:` override (warn)\nThe override read (`scripts\u002Fskill_entry.sh` plus a quoted-value match) requires double quotes, even where the yq scheduler accepts a bare `schedule:`. An unquoted `harness: grok` or `model: …` is silently ignored and the skill keeps running the global default.\n```bash\ngrep -nE '\\{[^}]*(harness|model):' aeon.yml | grep -vE '(harness|model): *\"'\n```\nEach printed line has an unquoted override → **warn**. Fix: quote it (`harness: \"grok\"`).\n\n### 11 · Category not one of the six — trips CI (warn)\nEvery skill's `category:` must be one of `core evolution basics dev crypto productivity` or the catalog CI gate goes red.\n```bash\nnode -e 'const fs=require(\"fs\");const V=[\"core\",\"evolution\",\"basics\",\"dev\",\"crypto\",\"productivity\"];for(const d of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+d+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const m=(fs.readFileSync(p,\"utf8\").split(\u002F^---$\u002Fm)[1]||\"\").match(\u002F^\\s*category:\\s*[\"\\x27]?([A-Za-z-]+)\u002Fm);if(!m||!V.includes(m[1]))console.log(d+\": category \"+(m?m[1]:\"(missing)\")+\" is not one of \"+V.join(\" \"))}'    # same set as scripts\u002Fcheck-skill-categories.sh\n```\nReport any offender as **warn**.\n\n### 12 · Daily-log heading not `### \u003Cslug>` — health loop can't key it (warn)\n`CLAUDE.md` mandates each skill append its daily-log entry under a **`### \u003Cslug>`** heading — \"the health loop parses this shape\", and `skill-health` \u002F `heartbeat` key skills by **slug**. A skill that logs under `## \u003CDisplay Name>` (wrong level *and* wrong identifier) still runs, but its narrative log is harder for the health view to attribute and for the cross-skill dedup (\"read the last 3 days of logs\") to match — a silent degrade, never an error.\n```bash\nnode -e 'const fs=require(\"fs\");const e=x=>x.replace(\u002F[.*+?^${}()|[\\]\\\\]\u002Fg,\"\\\\$&\");for(const s of fs.readdirSync(\"skills\")){const p=\"skills\u002F\"+s+\"\u002FSKILL.md\";if(!fs.existsSync(p))continue;const t=fs.readFileSync(p,\"utf8\");if(!\u002Fmemory\\\u002Flogs\\\u002F\\$\\{today\\}\u002F.test(t))continue;if(new RegExp(\"###\\\\s+\"+e(s)+\"\\\\b\").test(t))continue;const n=((t.match(\u002F^name:\\s*(.*)$\u002Fm)||[])[1]||s).trim();const h=t.match(new RegExp(\"^##\\\\s+(\"+e(s)+\"|\"+e(n)+\")\\\\b\",\"m\"));if(h)console.log(s+\" logs under \\\"\"+h[0]+\"\\\" - should be \\\"### \"+s+\"\\\"\")}'\n```\nEach hit → **warn**. Fix: change the Log-section heading (the instruction line *and* the example block) to `### \u003Cslug>`, and demote any sub-sections inside the block to `####`.\n\n## Report\n\n- **No findings → send nothing and exit.** A clean config is the common case; silence is correct and keeps this channel trustworthy.\n- **Findings → one consolidated `.\u002Fnotify`**, most-severe first. Write the body to a scratch file and send with `-f` (never a long argv):\n  ```bash\n  .\u002Fnotify -f \u003Cfile> \\\n    --title \"aeon-doctor: \u003CN> config issue(s)\" \\\n    --severity \u003Ccritical if any critical else warn> \\\n    --mute-key \"aeon-doctor\"\n  ```\n  Group by severity. For each finding give: the skill, one line on what breaks (and that it's **silent** — the operator won't see it in the Actions tab), and the exact one-command fix. If a `critical` exists, lead with it — an enabled skill that never fires is the whole reason this skill exists.\n- Do **not** open a PR or edit any file. Point mechanical fixes at `skill-repair` \u002F the `.\u002Faeon` CLI; leave the fix to the operator.\n\n## Constraints\n\n- Read-only by contract — inspect config, never mutate it. No `Write` \u002F `Edit` \u002F `git` \u002F `gh`.\n- Every finding must cite the **exact** file + line and a **copy-pasteable** fix. A config finding with no fix is noise.\n- Don't invent problems: only report what a check actually matched. If every check is clean, say nothing.\n- Fully local — no network, no secrets, no GitHub API. Run-outcome health is `skill-health`'s job; live attention is `heartbeat`'s. Stay in your lane: the **static config**.\n\n## Log\n\nThis skill is `read-only`, so the workflow's read-only guard writes its `### aeon-doctor` log entry from your captured output; a self-written entry would be a duplicate. Don't append to `memory\u002Flogs\u002F` yourself - put this record in your **final output** as bullets: checks run, findings by severity (or `clean`), and whether a notification was sent.\nEnd-states: `AEON_DOCTOR_CLEAN`, `AEON_DOCTOR_FINDINGS`, `AEON_DOCTOR_ERROR`.\n",{"name":5,"metadata":49,"description":7},{"var":50,"mode":34,"tags":51,"title":53,"category":26},"",[52,40],"meta","Aeon Doctor","main","ec7a50c7f73ef013905526393847a1ace55de01a","skills\u002Faeon-doctor",308,"https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon","https:\u002F\u002Fwww.aeon.fun\u002F","https:\u002F\u002Fgithub.com\u002Faeonfun","The most autonomous agent framework.","2026-11-15 03:40:18",{"source":8,"skillName":5,"commands":64,"prompt":74,"zipAvailable":75},[65,68,71],{"tool":66,"command":67},"npx","npx skills add aeonfun\u002Faeon --skill aeon-doctor",{"tool":69,"command":70},"bunx","bunx skills add aeonfun\u002Faeon --skill aeon-doctor",{"tool":72,"command":73},"pnpm","pnpm dlx skills add aeonfun\u002Faeon --skill aeon-doctor","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Faeon-doctor\u002FSKILL.md",true,[],[78,103,128,152,176,198],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":44},"01M70R8RA3SFXK2D1XP7XTJGAQ","sns-digest","Digest Internet Computer governance - new and open NNS proposals and SNS DAO proposals - so the operator sees what is up for a vote without watching the dashboard. Fully public, no secret; fires only when there is something new to report.","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Fsns-digest\u002FSKILL.md",[],[85,86,87,88,29,89,90,91,35,92,93,94,95,96,97,98,99,100,101,102],"sns","digest","sns digest","proposals","up","open","icp","new","internet","when","api","notifications","dao","crypto","dashboard","governance","report","write",{"skillNo":104,"name":105,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":112,"publishedAt":44},"01M70R5BMYW94KZ3XQM3MSWPC0","icp-deploy-check","Pre-deploy safety checklist for an Internet Computer Rust canister - read a local\u002Fcloned canister repo and verify the deploy-time footguns from the canister-security checklist (controller backup, anonymous-principal rejection, no mainnet fetch_root_key, upgrade safety) before it ships. Static, read-only, no network and no mainnet action.","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Ficp-deploy-check\u002FSKILL.md",[109],{"categoryNo":110,"categoryName":111,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署",[91,113,114,115,116,117,118,28,119,120,121,35,93,122,123,37,124,125,126,127],"icp deploy check","deploy check","check","deploy","fetch","anonymous","time","rust","local","upgrade","controller","Safety","network","action","security",{"skillNo":129,"name":130,"nameZh":6,"description":131,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":132,"categories":133,"keywords":142,"publishedAt":44},"01M70R312QZ9T236SZHGX8A8CM","canister-status","Monitor Internet Computer canisters for control-plane changes - a new controller, a module-hash change (upgrade), or a canister going missing from the public index - and alert the moment one happens. Uses only the public IC dashboard API.","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Fcanister-status\u002FSKILL.md",[134,137],{"categoryNo":135,"categoryName":136,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":138,"categoryName":139,"taxonomyType":140,"taxonomyTypeDesc":141},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[143,144,29,145,146,91,147,35,92,93,122,148,149,95,96,123,98,150,99,151],"canister status","status","missing","monitor","module","alert","plane","change","changes",{"skillNo":153,"name":154,"nameZh":6,"description":155,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":156,"categories":157,"keywords":160,"publishedAt":44},"01M6FVEXRG77BT4VH94P0S1TDT","you-web-search","Web search using You.com Search API with high-quality, cited results — runs keyless out of the box; YDC_API_KEY unlocks higher limits and real-time web crawling","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Fyou-web-search\u002FSKILL.md",[158,159],{"categoryNo":135,"categoryName":136,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":138,"categoryName":139,"taxonomyType":140,"taxonomyTypeDesc":141},[161,162,163,164,165,119,35,95,166,37,34,167,168,169,170,171,172,173,174,175],"web search","search","you","you web search","web","web crawling","real-time","basics","keyless","with","research","results","quality","high","limits",{"skillNo":177,"name":178,"nameZh":6,"description":179,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":180,"categories":181,"keywords":182,"publishedAt":44},"01M6FVER8Y2B0H7H26G4GTYK6N","write-tweet","Multi-format tweet studio - standalone drafts (10 across 5 size tiers), a 5-10 tweet thread, or 10 remixes of past tweets, selected via ${var}","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Fwrite-tweet\u002FSKILL.md",[],[102,183,184,185,95,186,187,168,188,189,190,191,192,193,194,195,196,197],"write tweet","social","studio","across","xai","twitter","multi","via","format","content","standalone","past","drafts","thread","size",{"skillNo":199,"name":200,"nameZh":6,"description":201,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":202,"categories":203,"keywords":207,"publishedAt":44},"01M6FVENCJ0HY2TEE6CNZN4F96","weekly-aeoncard","Build a weekly token-consumption recap image from memory\u002Ftoken-usage.csv - this week + all-time totals, top skills, rendered as a shareable SVG card.","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon\u002Fblob\u002Fmain\u002Fskills\u002Fweekly-aeoncard\u002FSKILL.md",[204],{"categoryNo":205,"categoryName":206,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[208,209,210,211,212,213,214,119,41,215,216,217,218,96,219,220,102,221,12,222],"weekly aeoncard","weekly","as","usage","SVG","memory","card","image","recap","csv","monitoring","productivity","build","token","top"]