[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6J3XF16MVB9TT2BSSFEMG30":3,"skill-same:01M6J3XF16MVB9TT2BSSFEMG30":80},{"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":45,"linkOnly":46,"markdownContent":47,"markdownBody":48,"frontMatter":49,"refName":56,"commitSha":57,"skillDirectory":58,"forksCount":59,"repositoryHtmlUrl":60,"repositoryHomepageUrl":61,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M6J3XF16MVB9TT2BSSFEMG30","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-agent","aeonfun","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F287715280?v=4","01M6CZ23C6MXGMF4N0V1DZ1247","aeon","01M6J3TN6FHJPWGPAR9ZVJY6FP",12,"MIT","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent\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,44],"doctor","aeon doctor","schedules","MCP","static","for","keys","mode","can","typos","correctness","read-only","duplicate","only","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":50,"description":7},{"var":51,"mode":35,"tags":52,"title":54,"category":55},"",[53,41],"meta","Aeon Doctor","evolution","main","abdce232d6142565b6afbdc4d4570354318e676d","skills\u002Faeon-doctor",4,"https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent","https:\u002F\u002Ft.me\u002Faeon_agent","https:\u002F\u002Fgithub.com\u002Faeonfun","https:\u002F\u002Fwww.aeon.fun\u002F","The most autonomous agent framework.","2026-11-14 00:38:03",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add aeonfun\u002Faeon-agent --skill aeon-doctor",{"tool":72,"command":73},"bunx","bunx skills add aeonfun\u002Faeon-agent --skill aeon-doctor",{"tool":75,"command":76},"pnpm","pnpm dlx skills add aeonfun\u002Faeon-agent --skill aeon-doctor","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent\u002Fblob\u002Fmain\u002Fskills\u002Faeon-doctor\u002FSKILL.md",true,[],[81,114,137,163,188,210],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":95,"publishedAt":45},"01M6J4ASEHAQYBZ5TZT4RVAM1Z","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-agent\u002Fblob\u002Fmain\u002Fskills\u002Fyou-web-search\u002FSKILL.md",[87,90],{"categoryNo":88,"categoryName":89,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":91,"categoryName":92,"taxonomyType":93,"taxonomyTypeDesc":94},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[96,97,98,99,100,101,37,102,103,38,35,104,105,106,107,108,109,110,111,112,113],"web search","search","you","you web search","web","time","api","web crawling","real-time","basics","keyless","with","research","results","high","quality","box","limits",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":45},"01M6J4AK1ZAR1HS398K0CWWBFX","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-agent\u002Fblob\u002Fmain\u002Fskills\u002Fwrite-tweet\u002FSKILL.md",[],[121,122,123,124,125,102,126,127,128,129,105,130,131,132,133,134,135,136],"write","write tweet","standalone","social","studio","across","xai","past","drafts","twitter","thread","multi","size","via","format","content",{"skillNo":138,"name":139,"nameZh":6,"description":140,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":141,"categories":142,"keywords":146,"publishedAt":45},"01M6J4AFYF6ZHHNQV6H7N92S8K","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-agent\u002Fblob\u002Fmain\u002Fskills\u002Fweekly-aeoncard\u002FSKILL.md",[143],{"categoryNo":144,"categoryName":145,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[147,148,149,150,151,152,153,101,42,154,155,156,157,158,159,160,161,121,162,12],"weekly","weekly aeoncard","as","usage","SVG","memory","card","image","recap","csv","monitoring","notifications","productivity","top","build","token",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":45},"01M6J4AAZF6D0YQ8NK92NYJSRX","vuln-tracker","One lifecycle poll over everything vuln-scanner produces - PR and advisory status, PVR triage transitions, and pending-disclosure aging, with a stars-secured impact headline and one action queue.","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent\u002Fblob\u002Fmain\u002Fskills\u002Fvuln-tracker\u002FSKILL.md",[],[170,171,42,172,173,121,162,174,175,176,177,178,179,180,181,182,183,107,184,185,186,187],"tracker","vuln tracker","impact","scanner","status","action","security","pr","stars","lifecycle","dev","gh","aging","advisory","github","transitions","triage","global",{"skillNo":189,"name":190,"nameZh":6,"description":191,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":192,"categories":193,"keywords":197,"publishedAt":45},"01M6J4A3ZM65VJNT4VEA9ESV39","vuln-scanner","Audit trending repos for real security vulnerabilities and disclose responsibly - scan and route findings (PVR \u002F dependency PR), re-submit queued advisories, and send armed email disclosures","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent\u002Fblob\u002Fmain\u002Fskills\u002Fvuln-scanner\u002FSKILL.md",[194],{"categoryNo":195,"categoryName":196,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000005","邮件与沟通",[173,198,199,200,201,42,202,176,203,29,204,180,184,205,187,206,102,207,208,209],"vuln scanner","resend","re","trending","repos","email","scan","vulnerabilities","submit","route","audit","send",{"skillNo":211,"name":212,"nameZh":6,"description":213,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":214,"categories":215,"keywords":217,"publishedAt":45},"01M6J49YHDRN3JNAN9GJW810P2","video-script","Turn a repo, product page, or update into a recording-ready video script in a receipts-first format - verifies every claim against live sources, then writes timestamped VO + on-screen direction with a plain-language glossary, assets checklist, anti-tells, and a verify-before-recording list","https:\u002F\u002Fgithub.com\u002Faeonfun\u002Faeon-agent\u002Fblob\u002Fmain\u002Fskills\u002Fvideo-script\u002FSKILL.md",[216],{"categoryNo":144,"categoryName":145,"taxonomyType":21,"taxonomyTypeDesc":22},[218,219,220,221,222,223,224,225,226,105,227,228,229,107,230,231,232,233,234,135],"Video","video script","script","recording","sources","ready","direction","page","repo","claim","plain-language","glossary","first","verify","product","screen","update"]