Canvas Health Check
SkillFiles & storageLint 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.
No other account needed.
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
-
Load project configuration:
- Read
.claude/diamonds/active.ymlforproduct_typeandproject_type - Read
${CLAUDE_PLUGIN_ROOT}/engine/canvas-guidance.ymlfor required/recommended/optional files per project type
- Read
-
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
-
Check
_metablocks:- For each existing canvas file, check for
_meta:block - Flag missing
_metablocks - Flag
last_validatedpast 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.applicabilitymarks it inapplicable (a value startingn/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 leavesFalseschema 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 removedlast_updatedto prevent. Precedents for the marker:dora-metrics.yml#sre(n/a-until-production) anddependency-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
versionfield missing or at 0
- For each existing canvas file, check for
-
Check confidence consistency:
- Gather all
confidence:values across canvas files - Flag confidence > 0.5 with
evidence_type: speculationorevidence_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.ymlconfidence - 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.)
- Gather all
-
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:
_metablocks in the field carry structural markers likeschema/assessment/not-yet-populated— flag these only when they appear OUTSIDE_meta; inside_metathey describe the file, not an evidence claim) - Flag evidence graded
anecdotalor better whose only sources are mocked personas /internal_simulated(honesty check — simulated evidence sits atspeculationon 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_classand compare against that, exactly as theevidence_typerule above already does. Do NOT hardcode the list here. Until v0.90.0 this step named five values while the schema had six;pointeris valid,check_source_independence.pyhandles 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_stakeholderevidence with confidence > 0.5 that hasvalidated: falseor novalidatedfield — stakeholder beliefs should not carry high confidence without external validation (Brown: organizational mythology) - Flag L2 opportunity canvas entries where ALL evidence is
internal_stakeholderorinternal_desk— no external human voice heard (Spool: secondhand research insufficient)
- Every canvas file with
-
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
-
Check evidence freshness (evidence decay):
- Scan all
provenanceblocks across canvas files forvalidated_atorcaptured_attimestamps - 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-interviewor/mycelium:log-evidenceto refresh." - Note: corrections and patterns do NOT decay — process learnings are timeless
- Scan all
7b. Check metric snapshot freshness (v0.14):
- If
.claude/jit-tooling/active-metrics.ymlexists, for eachstatus: activesource:- Find the newest snapshot in
.claude/evals/metrics/<source>/. - If >7 days old: warning ("[source] snapshot is [N] days old — run
/mycelium:metrics-pullto 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.").
- Find the newest snapshot in
- Also check per-adapter freshness: for each adapter file in
${CLAUDE_PLUGIN_ROOT}/jit-tooling/metrics-adapters/, iflast_known_workingis >180 days old, flag as warning suggesting regeneration viametrics-adapters/GENERATING.md. - Source: v0.14 metrics harvesting. Metric evidence has a faster staleness curve than interview evidence because the underlying data changes continuously.
- Check cross-reference integrity (leaf lifecycle):
- Every GIST idea with
source_leaf_id→ verify (a) the leaf exists inopportunities.ymland is not inarchived-solutions.ymlwithout the GIST being shelved, AND (b) the leaf's #1 riskiest assumption carries a recorded test verdict ofvalidated(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/invalidatedriskiest 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_loopentry withsource_leaf_id→ verify leaf exists - Flag broken references as warnings ("Zombie Solution" anti-pattern)
- Every GIST idea with
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.ymlexists:- 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_atset — flag if missing (orphan scenario with no origin) - Every scenario with
confidence > 0.5must have evidence sources — flag unsupported confidence - Every scenario referenced in
lifecycle.designed_against[]→ verify the solution exists inopportunities.ymlorgist.yml - Every scenario referenced in
lifecycle.tested_against[]→ verify test date is not in the future - Flag scenarios with
status: draftolder than 30 days (stale draft — either promote or discard) - Falsifiable-success check: every scenario with
statusother thandraftmust havesimulation.success_criteriawith at least one entry carryingobservable+threshold— flag a non-draft scenario whose success is only the qualitativesimulation.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 carryingconfidence > 0.3, must haveprovenance.source_classofexternal_humanorexternal_data— flag anyinternal_simulated/evidence_type: speculationscenario that is driving design or confidence (envision-only scenarios staystatus: draftuntil a real source grounds them; a fabricated scenario feels like research because it is a story)
- If
.claude/canvas/scenarios.ymldoes 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.ymlat scale L0–L3 (build-to-learn), lint itsdefinition_of_done.outcomeagainst 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 ofwithin ~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: watchingENTIRELY (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 whosestatusis non-terminal (NOTcompleted/abandoned/stalled), compute the latest activity date acrossupdated_at,reopened_at,touch_log[].date, andpartial_findings[].date. (reopened_atMUST count as activity — a task deliberately reopened today is fresh, not stale, even when its other dates are months old; the reopen convention writesreopened_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 omittedreopened_at.) THEN CHECKhorizon/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 skipwatchingand 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: markstalled, markabandoned, 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
statusISwaitingORwatching, 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 futurehorizondate, 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 topending/in_progresstasks. Then, for each non-terminal task, check whether it has already produced evidence — i.e. it has a populatedpartial_findingsblock, OR itscanvas_refsresolve to real evidence entries in purpose.yml/user-needs.yml dated at/after the task's activity. FIRST, CHECKhorizon/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-evidenceshould be closing the task going forward. -
(c) Consent-registry sync (best-effort; cross-source): if an attribution registry is available (
$MYCELIUM_ATTRIBUTION_REGISTRYor a private companion repo's.claude/memory/attribution-registry.yml), compare each contributor'sconsentvalue 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 anygeneric_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 withsource_class: external_humanwhoseprovenance.relationshipORprovenance.evidence_sources[]names an external contributor by name or handle. For each, check whether ANYhuman-tasks.ymlentry (pending OR completed) lists that name intarget_persona,touch_log, or abackfill_note. If no match, flag: "purpose.yml#L[NN] (or other canvas) records external_human evidence from [name] dated [date], buthuman-tasks.ymlhas no task covering this contributor. Backfill anht-XXXwith abackfill_noteso the channel is addressable for follow-ups, learning-target coupling, and consent tracking./mycelium:log-evidencev0.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 owed — DO 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