Canvas Health Check

SkillFiles & storage

Lint canvas files for staleness, missing fields, inconsistent evidence types, and orphaned references. Run periodically or before major transitions.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Canvas Health Check skill

What this skill tells your AI

The instructions your AI receives, as published by haabe/mycelium in plugins/mycelium/skills/canvas-health/SKILL.md and read by ahel’s review.

Audit the canvas knowledge base for quality, consistency, and completeness. The canvas is Mycelium's source of truth -- its quality directly determines agent output quality (Raschka: "context quality = model quality").

When to Use

  • Before any diamond phase transition (called automatically by /mycelium:diamond-assess)
  • After a period of inactivity (>7 days since last canvas update)
  • When agent output quality seems to degrade
  • After onboarding a new team member (ensures canvas is self-explanatory)
  • Proactively: run periodically to catch silent drift

Workflow

  1. Load project configuration:

    • Read .claude/diamonds/active.yml for product_type and project_type
    • Read ${CLAUDE_PLUGIN_ROOT}/engine/canvas-guidance.yml for required/recommended/optional files per project type
  2. Check file presence:

    • For each required canvas file: does it exist? Is it non-empty (>50 bytes)?
    • For each recommended canvas file: does it exist? Flag as gap if missing.
    • Report: N/M required files present, K recommended files missing
  3. Check _meta blocks:

    • For each existing canvas file, check for _meta: block
    • Flag missing _meta blocks
    • Flag last_validated past the horizon for that file's category, per the "Which threshold applies to which canvas file" table in ${CLAUDE_PLUGIN_ROOT}/engine/evidence-decay.md (strategic 180d, technical feasibility 120d, user-needs/competitive/market 90d, delivery metrics 30d, regulatory 365d; unlisted files fall back to 90d).
    • SKIP any file whose _meta.applicability marks it inapplicable (a value starting n/a, or naming a product type this project is not, or saying "NOT actively used"). Added v0.90.0. Two dogfood canvases held ZERO evidence — every metric field null, the only non-null leaves False schema defaults — and already said so in their own _meta. They decayed on a 30-day horizon anyway, and the only way to silence that warning would have been to date a validation of a file nobody uses: the manufactured-validation move v0.89.0 removed last_updated to prevent. Precedents for the marker: dora-metrics.yml#sre (n/a-until-production) and dependency-pins.yml (scope: local). This replaced a flat 30-day rule in v0.89.0. That rule was the only staleness number in this skill grounded in nothing — step 7 below has always used the decay table — and on the dogfood repo it flagged 20 of 25 canvases, including a strategic file validated 46 days earlier against a 180-day horizon. A check that fires on 80% of a corpus trains its reader to skip it.
    • Flag version field missing or at 0
  4. Check confidence consistency:

    • Gather all confidence: values across canvas files
    • Flag confidence > 0.5 with evidence_type: speculation or evidence_type: assumption
    • Flag confidence > 0.7 with fewer than 2 evidence sources
    • Flag confidence values that haven't changed across git history (anchored confidence anti-pattern)
    • Cross-check against .claude/diamonds/active.yml confidence
    • Cross-check the confidence RATIONALE prose too, not only the confidence: field. Projects record why a value moved in narrative fields (confidence_effect, confidence_effect_v2, or any prose block that names a number). Nothing reads those, so a canvas can be numerically consistent everywhere while the sentence explaining the number contradicts it. Flag when a rationale block names a confidence value that no longer matches .claude/diamonds/active.yml. Prose that encodes state is state, and it is otherwise unvalidated. (Roadmap dogfood 2026-08-02: the field said 0.10, the narrative beside it still said 0.25, and the numeric check passed.)
  5. Check evidence type consistency:

    • Every canvas file with evidence_type: should have it set to one of the shipped schema enum values (schemas/canvas/_common.schema.json#$defs/evidence_type — Gilad's evidence ladder): speculation, anecdotal, data-supported, test-validated, launch-validated
    • Flag unknown evidence types (note: _meta blocks in the field carry structural markers like schema / assessment / not-yet-populated — flag these only when they appear OUTSIDE _meta; inside _meta they describe the file, not an evidence claim)
    • Flag evidence graded anecdotal or better whose only sources are mocked personas / internal_simulated (honesty check — simulated evidence sits at speculation on the ladder regardless of how vivid it reads)
    • Every source_class: value must be one of the values in the SHIPPED SCHEMA enum — read ${CLAUDE_PLUGIN_ROOT}/schemas/canvas/_common.schema.json#$defs/source_class and compare against that, exactly as the evidence_type rule above already does. Do NOT hardcode the list here. Until v0.90.0 this step named five values while the schema had six; pointer is valid, check_source_independence.py handles it correctly, and a dogfood canvas used it 61 times. Following the prose produced 61 phantom violations — the documented-rule-diverges-from-enforcement class, in a step whose own sibling rule says to read the schema.
    • Flag internal_stakeholder evidence with confidence > 0.5 that has validated: false or no validated field — stakeholder beliefs should not carry high confidence without external validation (Brown: organizational mythology)
    • Flag L2 opportunity canvas entries where ALL evidence is internal_stakeholder or internal_desk — no external human voice heard (Spool: secondhand research insufficient)
  6. Check for orphaned references:

    • Canvas files that reference other canvas files (e.g., jobs-to-be-done.yml referencing opportunities.yml) -- verify the referenced file exists
    • Diamond references to canvas files -- verify they exist
  7. Check evidence freshness (evidence decay):

    • Scan all provenance blocks across canvas files for validated_at or captured_at timestamps
    • Compare against staleness thresholds from ${CLAUDE_PLUGIN_ROOT}/engine/evidence-decay.md:
      • User needs/interviews: 90 days
      • Competitive intelligence: 90 days
      • Strategic assumptions: 180 days
      • Technical feasibility: 120 days
      • DORA/delivery metrics: 30 days
    • Flag evidence past threshold as warning; past 3x threshold as critical
    • Suggest refresh actions: "Evidence in [file] is [N] days old. Run /mycelium:user-interview or /mycelium:log-evidence to refresh."
    • Note: corrections and patterns do NOT decay — process learnings are timeless

7b. Check metric snapshot freshness (v0.14):

  • If .claude/jit-tooling/active-metrics.yml exists, for each status: active source:
    • Find the newest snapshot in .claude/evals/metrics/<source>/.
    • If >7 days old: warning ("[source] snapshot is [N] days old — run /mycelium:metrics-pull to refresh").
    • If >30 days old: critical (evidence this stale is worse than no metric reference — anchors old state).
    • If missing entirely: info-level ("No snapshots yet for [source]. Run /mycelium:metrics-pull.").
  • Also check per-adapter freshness: for each adapter file in ${CLAUDE_PLUGIN_ROOT}/jit-tooling/metrics-adapters/, if last_known_working is >180 days old, flag as warning suggesting regeneration via metrics-adapters/GENERATING.md.
  • Source: v0.14 metrics harvesting. Metric evidence has a faster staleness curve than interview evidence because the underlying data changes continuously.
  1. Check cross-reference integrity (leaf lifecycle):
    • Every GIST idea with source_leaf_id → verify (a) the leaf exists in opportunities.yml and is not in archived-solutions.yml without the GIST being shelved, AND (b) the leaf's #1 riskiest assumption carries a recorded test verdict of validated (v0.54.0 — Torres selection: a GIST idea may only trace to a leaf that PASSED its assumption test, not one that merely exists or scored high on ICE). Flag a GIST idea whose source leaf has an untested / partial / invalidated riskiest assumption as a broken graduation (G-L2 REVIEW blocks it).
    • Every service entry with gist_id → verify that GIST idea exists
    • Every threat model entry with solution_id → verify that solution exists
    • Every go-to-market feedback_loop entry with source_leaf_id → verify leaf exists
    • Flag broken references as warnings ("Zombie Solution" anti-pattern)

7c. Run the do-not-cite register scan — do NOT re-derive this from prose:

python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_citations.py" --project-dir .

Reports any canvas line repeating a claim the project has already ruled against, printing the register entry VERBATIM. It reads .claude/harness/do-not-cite.yml; with no register it says NOTHING WAS CHECKED rather than passing.

Why it is a script and not a rule in this file. A do-not-cite register existed in one project's agent memory and failed twice in a single day (2026-09-01): once by not being read before a citation was written into two canvas files, and once by having a narrow entry PARAPHRASED into a broad one that was then acted on, rewriting four surfaces that were already correct. The rule was right both times and was not consulted at the moment of use.

It is not a lexical guard. It matches a curated list a human wrote, so it cannot fire on a claim nobody has ruled on. That distinction matters here: the sibling absence-claim-guard hook matches a prose signature and was consumer-measured at 29 lifetime fires, 16 in one day, and zero of that day's four confirmed errors caught. On first run against a real 25-file canvas this check found 3 true positives — including two in a file that an earlier manual correction pass on the same claims had missed entirely.

8b. Check scenario health (Hoskins):

  • If .claude/canvas/scenarios.yml exists:
    • Every scenario must have all three Hoskins elements populated (motivation, persona, simulation) — flag incomplete scenarios (corrected 2026-07-01: the model has THREE elements; the prior "persona/means/motive/simulation" was a distortion — "Means" is not a Hoskins element)
    • Every scenario must have lifecycle.born_at set — flag if missing (orphan scenario with no origin)
    • Every scenario with confidence > 0.5 must have evidence sources — flag unsupported confidence
    • Every scenario referenced in lifecycle.designed_against[] → verify the solution exists in opportunities.yml or gist.yml
    • Every scenario referenced in lifecycle.tested_against[] → verify test date is not in the future
    • Flag scenarios with status: draft older than 30 days (stale draft — either promote or discard)
    • Falsifiable-success check: every scenario with status other than draft must have simulation.success_criteria with at least one entry carrying observable + threshold — flag a non-draft scenario whose success is only the qualitative simulation.success_state (an un-falsifiable scenario is a user story, not a Hoskins scenario; it cannot be run against the eval-runner)
    • Grounding check: a scenario listed in any lifecycle.designed_against[], OR carrying confidence > 0.3, must have provenance.source_class of external_human or external_data — flag any internal_simulated / evidence_type: speculation scenario that is driving design or confidence (envision-only scenarios stay status: draft until a real source grounds them; a fabricated scenario feels like research because it is a story)
  • If .claude/canvas/scenarios.yml does NOT exist but project_type requires it (per ${CLAUDE_PLUGIN_ROOT}/engine/canvas-guidance.yml): flag as warning

8c. Check build-mode (Patton/Cagan — the /define-done build-mode gate's unconditional backstop):

  • For each diamond in active.yml at scale L0–L3 (build-to-learn), lint its definition_of_done.outcome against an earn-verb lexicon (deploy, ship, releas, production, go live, roll out, launch, all users).
  • Ignore a match that sits inside a NEGATION (not, NOT, never, rather than, instead of within ~40 characters before it). Added v0.90.0: this fired on two dogfood diamonds whose outcomes read "NOT shipped-a-product-through-it" and ("Bet validated", not "shipped"). Both explicitly disclaim being earn-bars, and saying so is what tripped the check built to catch earn-bars. Left unfixed it fires on every well-written build-to-learn DoD, because disclaiming the earn-bar is the natural way to write one.
  • On a match → WARN (do not auto-fail — this is a keyword tripwire, not the semantic adjudication): "Possible build-to-earn goal on a build-to-learn (L0–L3) diamond. Confirm this is a ship-to-LEARN outcome (disposable / opt-in, the learning is the done-bar) and not a premature earn-bar. If it's an earn-bar, re-run /define-done — production rollout is the L4 outcome."
  • This converts the birth-only, agent-adjudicated gate into a check that fires regardless of whether the run engaged the gate prose. It routes the semantic call back to a human/agent; it never adjudicates "earn-shaped" itself (that stays with /define-done).

8c. Human-task reconciliation (added v0.31.3, closes the evidence/status/consent decoupling drift — corrections.md 2026-05-28):

The failure this catches: a fact about a human-task lives in 2+ places (the task status, the evidence file it produced, the contributor's consent registry) and only the salient one gets updated, so the canvas silently drifts from reality. Three sub-checks over .claude/canvas/human-tasks.yml#pending_tasks:

  • (a) Status-vs-activity staleness: SKIP status: watching ENTIRELY (v0.132.0) — a watch owes nothing, so "untouched 90d" is its CORRECT state and flagging it is a false positive that teaches the reader to ignore this check. Before the value existed, projects hand-exempted their watches from staleness rules in prose, which is the evidence the state is real. For each remaining task whose status is non-terminal (NOT completed/abandoned/stalled), compute the latest activity date across updated_at, reopened_at, touch_log[].date, and partial_findings[].date. (reopened_at MUST count as activity — a task deliberately reopened today is fresh, not stale, even when its other dates are months old; the reopen convention writes reopened_at, so omitting it here mis-flags every reopened task. Dogfood 2026-07-05: ht-003, reopened that day, read as 70d stale because this list omitted reopened_at.) THEN CHECK horizon / scoring_horizon, EXACTLY AS (b) DOES. If it is in the FUTURE, do not flag — a dated horizon IS the recorded reason the task is open and quiet, which is precisely what this sub-check asks the author to supply. Added v0.174.0 after the 2026-09-03 dogfood run flagged 11 tasks and all 11 had future horizons — a 100% false-positive rate, against this skill's own standard that a check firing on most of a corpus trains its reader to skip it. The repair already existed one sub-check away: v0.90.0 wrote this exemption into (b) for the identical reason ("the check demanded a reason that was already written in the field beside it") and it was never carried across. (a) was edited again at v0.132.0 to skip watching and the horizon exemption still did not travel — one rule, two adjacent sub-checks, one repaired. A PASSED horizon is the opposite case and ELEVATES the flag. A task sitting untouched past its OWN stated deadline is the strongest version of this finding, not an exempt one — say so in the message rather than treating it like any other stale task. If the latest activity is >21 days ago (or there is no activity date at all) AND no future horizon is set, flag: "ht-XXX untouched [N]d while still [status][, and [N]d past its own horizon of [date]] — decide: mark stalled, mark abandoned, or nudge the contact. Abandonment is a non-event; nothing else will surface this." (The session-start hook flags this at 14d for awareness; canvas-health is the deeper 21d decision prompt.)

  • (b) Evidence-exists-but-task-open: FIRST, IF status IS waiting OR watching, THAT IS THE RECORDED REASON THE TASK STAYS OPEN — do not flag (v0.132.0). This sub-check used to infer "legitimately waiting" from a future horizon date, which is a proxy for a state the author can now simply declare; an explicit status beats a date a reader has to interpret. The horizon fallback below still applies to pending/in_progress tasks. Then, for each non-terminal task, check whether it has already produced evidence — i.e. it has a populated partial_findings block, OR its canvas_refs resolve to real evidence entries in purpose.yml/user-needs.yml dated at/after the task's activity. FIRST, CHECK horizon / scoring_horizon. If it is in the FUTURE, do not flag — a dated horizon IS the recorded reason the task stays open, which is precisely what this sub-check asks the author to supply. Added v0.90.0: the 2026-08-05 dogfood run flagged seven tasks and five had future horizons, so the check demanded a reason that was already written in the field beside it. Only the two horizonless tasks were real findings, and the right remedy for those is a horizon (see the ht-027 precedent), not closure. If evidence exists, the task is still open, AND no future horizon is set, flag: "ht-XXX has captured evidence (partial_findings / linked purpose.yml entry) but status is [status] — close it (completed) or record why it stays open. Logging evidence and closing the source task are separate steps; this catches the gap." Recommend /mycelium:log-evidence should be closing the task going forward.

  • (c) Consent-registry sync (best-effort; cross-source): if an attribution registry is available ($MYCELIUM_ATTRIBUTION_REGISTRY or a private companion repo's .claude/memory/attribution-registry.yml), compare each contributor's consent value there against any consent state recorded in the agent's auto-memory (~/.claude/projects/<id>/memory/). Flag mismatches: "Consent for [name] is [X] in the registry but [Y] in auto-memory — the registry is canonical (Check 33 reads it); sync them." If neither source is accessible in the current context, skip this sub-check and note it was skipped. Do NOT print the literal value of any generic_only/project-name carve-out term into the report.

  • (d) Untracked-channel evidence (added v0.39.10, symmetric inverse of (b) — closes the drift where outreach produces evidence with no source-task at all): scan recent evidence entries across .claude/canvas/*.yml (windowed to entries dated within the last 30 days) for items with source_class: external_human whose provenance.relationship OR provenance.evidence_sources[] names an external contributor by name or handle. For each, check whether ANY human-tasks.yml entry (pending OR completed) lists that name in target_persona, touch_log, or a backfill_note. If no match, flag: "purpose.yml#L[NN] (or other canvas) records external_human evidence from [name] dated [date], but human-tasks.yml has no task covering this contributor. Backfill an ht-XXX with a backfill_note so the channel is addressable for follow-ups, learning-target coupling, and consent tracking. /mycelium:log-evidence v0.39.10+ catches this at log-time; older entries may need retroactive backfill." Skip names already flagged as registry-private (generic_only). NUDGE-tier, not gating.

  • (e) Reply owedDO NOT re-derive this from prose. Run the script:

python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_reply_owed.py" --project-dir .
 This step used to describe the algorithm here, in parallel with an
 implementation inside `hooks/session-start.sh`. **One rule, two implementations,
 and only one of them could be executed.** The same-day tie-break was diagnosed
 2026-08-05, written into THIS PROSE at v0.90.0, and left live in the hook — which
 flagged the same two tasks (ht-060, ht-003) again on 2026-08-07, two days later.
 A rule repaired in the copy that cannot run is not repaired.

 So the logic now lives in `scripts/check_reply_owed.py` and both surfaces call it.
 What it does, for reading rather than for reimplementing: it walks `touch_log[]`,
 considers only CONTACT directions (`internal` notes are not contact and must not
 mask an inbound), takes the newest by date with **ties broken by log position**
 — dates are day-granular and carry no ordering within a day — and flags a task
 whose newest contact is `inbound` and ≥3 days old. An explicit `reply_owed:`
 field forces the flag. The tiebreak is position rather than direction because
 preferring outbound on a tie would silence the honest case where they wrote BACK
 the same day.

 **Why this is not covered by `(a)`.** `(a)` treats any `touch_log` entry as
 activity, so an inbound REFRESHES the staleness clock — a task where the contact
 is waiting on you scores as HEALTHIER than one where you sent something and heard
 nothing. Opposite states, indistinguishable. Dogfood 2026-08-01: three unanswered
 inbounds aged 4–7 days sat invisible behind a green staleness pass.

 **Migration nudge**: entries lacking `direction` are invisible to this check and
 to `session-start`. Pre-v0.68.0 logs predate the contract, so suggest backfilling
 and never fail on it. NUDGE-tier.

Output all (a)–(d) as warnings (not critical) — they are drift, not breakage. Each names the specific ht-ID (or missing-ht contributor) and the specific action.

8d. Learning-target coupling on feedback tasks (added v0.31.6, closes the "we asked for feedback but didn't ask what we needed to learn" gap — see engine/canvas-guidance.yml#learning_target_coupling):

The failure this catches: the canvas carries open learning needs (ON HOLD / RE-GATED action flags, in-progress human-tasks naming a MISSING SIGNAL, low-confidence entries with an un-validated assumption), AND a feedback-gathering task is open, but the task's questions target none of those gaps — so the feedback returns whatever the respondent volunteers rather than the answers the canvas is waiting on. Feedback capacity is scarce and non-repeating; an un-targeted session spends it without retiring any gap.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
45
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
canvas-health
Source
github.com/haabe/mycelium