技能 Freshness
- 作者仓库星标 430
- 作者仓库 aeon
${var} — Optional. Pass
dry-runto skip the notification (article still writes, log still appends). Pass a single skill name to scope the audit to that one consumer (e.g.var=tweet-allocator). Empty = audit every enabled skill inaeon.yml.
Today is ${today}. Walk every enabled skill in aeon.yml, parse the file dependencies it declares (explicit chains: consume: edges + implicit articles/, .outputs/, memory/topics/, memory/state/ references inside each SKILL.md), check the on-disk freshness of each dependency against a per-class threshold, and surface a single decision-ready report: which enabled consumer is about to read a file that's older than its expected freshness window.
The skill answers a question the existing health stack cannot: a chained skill that runs on schedule, with no API errors, and a 100% pass rate, can still silently act on stale upstream data if the producer skill failed earlier and nothing replaced its output. Today there is no check that tweet-allocator reading articles/token-report-*.md is reading today's version rather than last Tuesday's. This skill closes that gap.
Why this exists
Aeon's reliability story has three layers — heartbeat (per-run pulse), skill-analytics (per-skill ranking over time), skill-health (per-skill failure detection) — and one gap. None of them catches the case where a producer skill's last successful run was N days ago and a downstream consumer is still happily reading the cached file as if it were fresh. The output of tweet-allocator looks normal. The output of repo-pulse looks normal. The aggregate verdict from operator-scorecard looks normal. The only signal something is wrong is that the upstream articles/token-report-*.md mtime drifted past its freshness window — and nobody is looking.
This skill looks. It's a watchdog for silent staleness, not for failures. It does not duplicate skill-health's job (which catches consecutive failures by reading run history) or skill-update-check's job (which catches upstream SKILL.md drift in imported skills). Its scope is narrow: file-on-disk freshness vs the consumer that's about to read it.
Config
No new secrets. No new env vars. No new state file beyond memory/topics/skill-freshness-state.json for prior-run dedup.
Reads:
aeon.yml— enabled skill list,chains:blocks (steps, consume, parallel), per-skillschedule(used to derive expected freshness windows).- Every
skills/*/SKILL.mdwhose correspondingaeon.ymlentry hasenabled: true— for implicit file-reference extraction. articles/,.outputs/,memory/topics/,memory/state/— directory listings + mtimes only (no content reads beyond what's needed for fingerprinting).
Writes:
articles/skill-freshness-${today}.md— the report.memory/topics/skill-freshness-state.json— fingerprint + last-verdict for run-to-run dedup.memory/logs/${today}.md— log block.
No outbound HTTP. No gh api calls. No env-var-in-headers. Pure local file I/O.
Freshness thresholds
The threshold for a dependency depends on its path class:
| Path class | Threshold | Rationale |
|---|---|---|
articles/{skill}-*.md |
28 hours | Daily skills run once per day; 28h gives a 4h grace window for clock skew + run delays. |
articles/{skill}-*.md produced by a weekly skill (cron starts with 0 _ * * 0-6 only) |
8 days (192h) | Weekly producers have a 24h grace window. |
.outputs/{skill}.md (chain runner outputs) |
4 hours | Chain steps run minutes apart; a 4h-old .outputs/ file is a stale chain run. |
memory/topics/{name}.md |
7 days (168h) | Topic files are reference material, edited on memory-flush cycles (~weekly). |
memory/state/{name}.json |
30 days (720h) | State files are append/update-on-write; 30 days is a "skill hasn't run at all" signal. |
Per-class thresholds are computed at runtime — not hardcoded per dependency. The skill discovers the producer's schedule from aeon.yml and picks the daily-vs-weekly bucket automatically.
Severity bands per dependency:
OK— file mtime within threshold.WARN— file mtime past threshold but ≤ 2× threshold.STALE— file mtime past 2× threshold (real degradation, not a one-day blip).MISSING— referenced file does not exist on disk at all.
MISSING only fires for explicit dependencies (chains: consume: entries + canonical articles/{producer}-${today}.md patterns). Implicit grep-discovered references that simply never existed are not flagged — many SKILL.md files mention paths in pseudocode or comments that aren't real reads.
Steps
1. Parse var and resolve scope
- If
${var}matches^dry-run→MODE=dry-run. Strip the prefix; remainder treated as scope override. - Otherwise
MODE=execute. - If the remaining var is a non-empty string that matches an
aeon.ymlskill key →SCOPE=single,SCOPED_SKILL=$var. If it doesn't match any key, logSKILL_FRESHNESS_NO_MATCH: ${var} not in aeon.ymland exit (no notify, no article). - Otherwise
SCOPE=fleetand audit every enabled skill.
2. Load enabled-skill list and build the producer index
Parse aeon.yml. Build two maps:
ENABLED— set of skill names whereenabled: true. (Skills withenabled: falseare not audited as consumers — their dependencies don't matter until they're turned on. They CAN appear as producers though, and their freshness is still tracked since other consumers may depend on them.)PRODUCER_CADENCE— map skill_name →daily|weekly|on_demandderived from the cron expression:- cron with
* * *in days/months/weekdays →daily - cron whose weekday field matches
^[0-6]$(single weekday) →weekly workflow_dispatchor empty →on_demand(skipped from freshness audit; on-demand outputs have no expected cadence)
- cron with
3. Gather explicit dependencies (chains: consume:)
Walk aeon.yml chains: blocks. For each step with a consume: [...] list, the consuming skill depends on .outputs/{producer}.md for each named producer. Record these as explicit edges with class outputs (4h threshold).
Also record any step with parallel: [...] followed by a downstream consume: reference as the same class.
4. Gather implicit dependencies (grep over enabled SKILL.md files)
For each skill in ENABLED, read its SKILL.md and extract every reference to:
articles/[a-zA-Z0-9_-]+(-\$\{today\}|-[0-9]{4}-[0-9]{2}-[0-9]{2})?\.md
\.outputs/[a-zA-Z0-9_-]+\.md
memory/topics/[a-zA-Z0-9_.-]+\.md
memory/state/[a-zA-Z0-9_.-]+\.json
Filter out:
- References inside fenced code blocks marked
bashortextthat are clearly examples (e.g.# example: articles/foo-2026-01-01.md). - References to the consumer's own output paths (a producer self-reading its prior file is not a freshness gap; that's its own state-keeping). Detected when the producer prefix matches the consuming skill name.
- References inside the comment marker
<!-- skill-freshness:ignore -->and the next line (escape hatch for SKILL.md authors who cite a path in prose without actually reading it).
Each surviving reference becomes an implicit edge with the appropriate path class.
5. Resolve canonical "today's article" patterns
For every articles/{producer}-${today}.md reference (or the date-suffixed equivalent), resolve to the actual most-recent file on disk: ls -1t articles/{producer}-*.md 2>/dev/null | head -1. Record the resolved path AND the producer's expected cadence (from step 2's PRODUCER_CADENCE map).
If no file matches the pattern at all, record as MISSING (only counted if the producer has cadence daily or weekly — on_demand producers may legitimately have never run).
6. Score each dependency
For every (consumer, dependency) pair:
mtime_age_hours = (now - file.mtime) in hours
threshold_hours = lookup_threshold(path_class, producer_cadence)
severity = OK if mtime_age_hours <= threshold_hours
| WARN if mtime_age_hours <= 2 * threshold_hours
| STALE if mtime_age_hours > 2 * threshold_hours
| MISSING if file does not exist (and edge is explicit OR pattern-canonical)
Aggregate per-consumer:
consumer_verdict = WORST severity across all its dependencies
MISSING > STALE > WARN > OK for the rollup.
7. Roll up to the fleet verdict
fleet_verdict = WORST consumer_verdict across all enabled consumers
Translation to exit status:
| fleet_verdict | exit_status |
|---|---|
| OK across the board | FRESHNESS_OK |
| At least one WARN, no STALE / MISSING | FRESHNESS_WARN |
| At least one STALE OR MISSING | FRESHNESS_STALE |
8. Dedup vs prior run
Compute a stable verdict fingerprint: sha1sum of the sorted list of consumer:dep:severity triples (excluding OK rows — only flagged rows count toward the fingerprint).
Compare against memory/topics/skill-freshness-state.json last_flagged_fingerprint. If identical AND today's fleet_verdict is the same as last_verdict:
- Article still writes (idempotent same-day overwrite).
memory/topics/skill-freshness-state.jsonupdates thelast_run_attimestamp.- Notify is suppressed with status
FRESHNESS_NO_CHANGE— no point pinging the operator about the same stale file two days running. The state expires after 7 days; if nothing has changed for a week, the next run will re-emit the notification as a periodic reminder.
If different (a new flag appeared, an old one cleared, or the verdict band changed): notify normally.
9. Write the article
Path: articles/skill-freshness-${today}.md. Overwrite if exists.
# Skill Freshness — ${today}
**Verdict:** ${verdict_emoji} ${fleet_verdict} — ${one_line_summary}
*Audited ${enabled_count} enabled skills · ${dependency_count} dependencies checked · ${flagged_count} flagged*
## Flagged dependencies
| Consumer | Dependency | Class | Age | Severity |
|----------|-----------|-------|-----|----------|
| ${consumer} | `${path}` | ${class} | ${age_human} | ${severity_emoji} ${severity} |
| ... | | | | |
(Sorted by severity desc, then consumer name. Omit OK rows entirely — they are noise.)
## What this means per consumer
For every consumer whose verdict ≠ OK, one paragraph:
> **${consumer}** — depends on ${N} files; ${flagged_count} flagged. Worst: `${worst_path}` last updated ${age} ago (threshold ${threshold}h, class ${class}). The producer `${producer}` last successful run: ${producer_last_run_or_unknown}. Suggested action: ${one_line_suggestion}.
`one_line_suggestion` is a small lookup:
- `MISSING` + producer is `daily`/`weekly` → "Check `${producer}` run history with `./scripts/skill-runs --skill ${producer} --hours 168`."
- `STALE` → "Verify `${producer}` is still on schedule; if so, the producer ran but did not write a new article."
- `WARN` → "Monitor — one missed run, expected to clear on next producer cadence."
## Healthy consumers
A one-line per consumer with verdict OK: `- ${consumer} — ${dep_count} deps, all fresh.`
Cap at 8 entries; collapse the rest into `+ N more all-fresh consumers.` to keep the article scannable.
## Source status
- `aeon.yml`: ${parsed_skill_count} entries, ${enabled_count} enabled
- Implicit references discovered: ${implicit_count}
- Explicit `chains: consume:` edges: ${explicit_count}
- Files not yet on disk (skipped — implicit references that never existed): ${ignored_count}
---
*Companion to `skill-health` (per-skill failure detection) and `heartbeat` (per-run pulse). This skill catches the silent-staleness gap those two cannot: a consumer reading a stale file with no API errors and a 100% pass rate. Methodology: every age and threshold is computed from on-disk mtimes — this skill measures nothing it does not also report.*
10. Persist state
Write memory/topics/skill-freshness-state.json:
{
"last_run_at": "${ISO timestamp}",
"last_verdict": "${fleet_verdict}",
"last_flagged_fingerprint": "${sha1}",
"consumer_count": ${enabled_count},
"dependency_count": ${dependency_count},
"flagged_count": ${flagged_count},
"first_seen_at": {
"${consumer}:${path}": "${ISO timestamp}"
}
}
first_seen_at records when each currently-flagged dep first crossed its threshold. Reused on the next run to detect "this has been stale for >7 days" — escalate one severity band in that case (WARN → STALE if persistent).
Cap first_seen_at to 200 entries; drop oldest by timestamp.
11. Send notification
If MODE == dry-run: skip notify, log FRESHNESS_DRY_RUN, exit.
If fleet_verdict == FRESHNESS_OK: log FRESHNESS_OK, do not notify (no news is good news; a green daily ping is noise).
If fleet_verdict ∈ {WARN, STALE} AND fingerprint changed since last run: notify.
If fingerprint identical to last run AND last run was within 7 days: log FRESHNESS_NO_CHANGE, do not notify.
Notification body:
*Skill Freshness — ${today}*
${verdict_emoji} ${fleet_verdict} — ${flagged_count} of ${dependency_count} deps flagged across ${affected_consumer_count} of ${enabled_count} enabled consumers
Worst:
- ${consumer_1} ← ${path_1} (${age_1} old, class ${class_1}, sev ${sev_1})
- ${consumer_2} ← ${path_2} (${age_2} old, class ${class_2}, sev ${sev_2})
- ${consumer_3} ← ${path_3} (${age_3} old, class ${class_3}, sev ${sev_3})
Action: ${one_line_action_for_worst_consumer}
Full: articles/skill-freshness-${today}.md
Cap message at ~3500 chars. Drop "Worst" entries 4+ if exceeded.
12. Log to memory/logs/${today}.md
## Skill Freshness
- **Skill**: skill-freshness
- **Verdict**: ${verdict_emoji} ${fleet_verdict}
- **Audited**: ${enabled_count} enabled consumers · ${dependency_count} deps · ${flagged_count} flagged
- **Worst**: ${consumer_with_worst_severity} — ${worst_path} (${worst_age} old, ${worst_severity})
- **Article**: articles/skill-freshness-${today}.md
- **Notification sent**: ${yes|no — FRESHNESS_OK|no — FRESHNESS_NO_CHANGE|no — dry-run}
- **Status**: ${FRESHNESS_OK|FRESHNESS_WARN|FRESHNESS_STALE|FRESHNESS_NO_CHANGE|FRESHNESS_DRY_RUN}
Exit taxonomy
| Status | Meaning | Notify? |
|---|---|---|
FRESHNESS_OK |
every enabled consumer's deps are fresh | No (silence is the signal) |
FRESHNESS_WARN |
at least one dep past 1× threshold but no STALE/MISSING | Yes (only on fingerprint change) |
FRESHNESS_STALE |
at least one dep past 2× threshold OR a canonical-pattern dep MISSING | Yes (only on fingerprint change) |
FRESHNESS_NO_CHANGE |
flagged set identical to prior run, last run < 7 days ago | No (re-emits after 7d) |
FRESHNESS_DRY_RUN |
var=dry-run mode |
No (article still writes) |
SKILL_FRESHNESS_NO_MATCH |
${var} named a skill not in aeon.yml |
No |
Sandbox note
Pure local file I/O — no curl, no gh api, no env-var-in-headers, no prefetch script. Every read is a directory listing or an mtime call; every write is to articles/, memory/topics/, or memory/logs/. Works in the GitHub Actions sandbox without any of the network workarounds other skills need. The only outbound call is ./notify itself, which is already sandbox-safe (postprocess-notify pattern).
Constraints
- Read-only across producers. This skill never re-runs a producer to refresh its output, never deletes stale files, never edits another skill's SKILL.md. It reports; the operator (or
skill-repair) acts. - Enabled consumers only. A skill with
enabled: falsedoes not need its dependencies audited — it isn't going to consume them. This keeps the report scoped to what's actually live in the schedule. - Implicit dependencies are best-effort. Grep-based discovery is heuristic. False positives are tolerated (consumer paragraph clarifies why); false negatives are accepted (an explicit
chains: consume:edge is the source of truth for chain runs). The goal is to surface the worst-case staleness, not to prove formally complete coverage. - Per-class thresholds, not per-skill. The threshold for
articles/token-report-*.mdis the same as forarticles/repo-pulse-*.md: the path class drives the window, derived from the producer's cadence inaeon.yml. This keeps the table maintainable as the fleet grows. - Fingerprint-based dedup. A stale file flagged today and still stale tomorrow does not re-notify. The 7-day re-emit window handles the case where a chronic stale file has been forgotten about.
- No issue filing. Anomalies surface in the verdict and the article. Persistence and resolution belong to
skill-health. This skill is read-only acrossmemory/issues/. - Idempotent. Same-day reruns overwrite the article and state file. The log entry appends one block per run.
- 流狐分类
- 安全 · meta · dev
- 作者声明 Agent
- 未找到明确声明;不据此推断已兼容或已测试
- 静态检查
- 94 / 100 · 启发式扫描,不代表运行安全
- 作者 / 版本 / 许可
- @aaronjmars · 未声明 license
- 流狐 Token 估算
- 低消耗
- 流狐接入估算
- 需简单配置
- 是否需要外部 API Key
- 未发现要求
- 检测到的系统要求
- Windows
- 底层运行要求
- 未声明
- 检测到的文件与系统行为
-
- 只读
- 允许写入 / 修改
- Shell 执行
- 读取环境变量
- 检测到的网络行为
- 允许外网请求
- 安装命令数
- 无(仅作为资料)
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
作者没有在当前 SKILL.md 中定义固定输出样例。 Aeon's reliability story has three layers — heartbeat (per-run pulse), skill-analytics (per-skill ranking over time), skill-health (per-skill failure detection) — and one gap. None of them catches the case where a producer skill's last successful run was N…
No new secrets. No new env vars. No new state file beyond memory/topics/skill-freshness-state.json for prior-run dedup. Reads: aeon.yml — enabled skill list, chains: blocks (steps, consume, parallel), per-skill schedule (used to derive expected freshness…
The threshold for a dependency depends on its path class: Path class · Threshold · Rationale articles/{skill}-.md · 28 hours · Daily skills run once per day; 28h gives a 4h grace window for clock skew + run delays.
Steps
If ${var} matches ^dry-run → MODE=dry-run. Strip the prefix; remainder treated as scope override. Otherwise MODE=execute. If the remaining var is a non-empty string that matches an aeon.yml skill key → SCOPE=single, SCOPEDSKILL=$var. If it doesn't match any…
Parse aeon.yml. Build two maps: ENABLED — set of skill names where enabled: true. (Skills with enabled: false are not audited as consumers — their dependencies don't matter until they're turned on. They CAN appear as producers though, and their freshness is…
> **${var}** — Optional. Pass `dry-run` to skip the notification (article still writes, log still appends). Pass a single skill name to scope the audit to that one consumer (e.g. `var=tweet-allocator`). Empty = audit every enabled skill in `aeon.yml`.
Today is ${today}. Walk every enabled skill in `aeon.yml`, parse the file dependencies it declares (explicit `chains: consume:` edges + implicit `articles/`, `.outputs/`, `memory/topics/`, `memory/state/` references inside each `SKILL.md`), check the on-disk freshness of each dependency against a per-class threshold, and surface a single decision-ready report: which enabled consumer is about to read a file that's older than its expected freshness window.
The skill answers a question the existing health stack cannot: a chained skill that runs on schedule, with no API errors, and a 100% pass rate, can still silently act on stale upstream data if the producer skill failed earlier and nothing replaced its output. Today there is no check that `tweet-allocator` reading `articles/token-report-*.md` is reading today's version rather than last Tuesday's. This skill closes that gap.
## Why this exists
Aeon's reliability story has three layers — `heartbeat` (per-run pulse), `skill-analytics` (per-skill ranking over time), `skill-health` (per-skill failure detection) — and one gap. None of them catches the case where a producer skill's last successful run was N days ago and a downstream consumer is still happily reading the cached file as if it were fresh. The output of `tweet-allocator` looks normal. The output of `repo-pulse` looks normal. The aggregate verdict from `operator-scorecard` looks normal. The only signal something is wrong is that the upstream `articles/token-report-*.md` mtime drifted past its freshness window — and
… 作者原文负责流程事实;流狐只索引当前章节、要点、文件与命令。
章节 -> Why this exists → Config → Freshness thresholds → Steps → 1. Parse var and resolve scope → 2. Load enabled-skill list and build the producer index
要点 -> ${var} · silent staleness · Severity bands per dependency · explicit · implicit · suppressed · Verdict · ${consumer}
文件/命令 -> dry-run · var=tweet-allocator · aeon.yml · chains: consume: · articles/ · .outputs/ · memory/topics/ · memory/state/
内容 SHA-256 -> 7928c25ff03d
方法与流程
适用与边界
原文中的明确线索
dry-run、var=tweet-allocator、aeon.yml、chains: consume:、articles/、.outputs/、memory/topics/、memory/state/