canvas-health — Claude Code skill: what it does, how to install
Lints canvas knowledge base files for stale validation dates, missing fields, and evidence mismatches. Keeps model context consistent.
npx skills add haabe/mycelium --skill canvas-health -a claude-codeWhat canvas-health does
The skill audits canvas knowledge base files inside the Mycelium framework. It reads project settings from .claude/diamonds/active.yml and checks file presence against canvas-guidance.yml. Empty files smaller than 50 bytes get flagged immediately.
Next, the tool inspects each _meta block and compares last_validated dates against category decay limits. These ranges span 30 days for delivery metrics up to 365 days for regulatory files. The linter catches confidence drift. It flags confidence scores above 0.5 backed by pure speculation, or scores above 0.7 with only one source. It also compares numeric fields against narrative explanation text. When written justification contradicts the active number, the check alerts you. Context drift corrupts agent decisions.
This skill assumes Mycelium harness behavior and skips state checks when run standalone. Running it before locking key project decisions catches silent context drift early.
What it helps with
- detect expired validation dates in canvas files
- catch mismatches between numeric confidence and rationale text
- identify missing required canvas documents for the project type
- flag high-confidence claims supported only by speculation or mock personas
Example prompts for the agent
- Run canvas-health on the project canvas files
- Audit canvas consistency before recording the decision
- Check canvas files for stale evidence and confidence mismatches
How to install canvas-health
1Skills CLI (Vercel)
npx skills add haabe/mycelium --skill canvas-health -a claude-code2Manually: put the files in place
~/.claude/skills/canvas-health/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/canvas-health/
1Skills CLI (Vercel)
npx skills add haabe/mycelium --skill canvas-health -a codex2Manually: put the files in place
.agents/skills/canvas-health/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add haabe/mycelium --skill canvas-health -a cursor2Manually: put the files in place
.cursor/skills/canvas-health/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install haabe/mycelium canvas-health2Manually: put the files in place
.github/skills/canvas-health/SKILL.md1Skills CLI (Vercel)
npx skills add haabe/mycelium --skill canvas-health -a gemini-cli2Platform CLI
gemini skills install https://github.com/haabe/mycelium --path plugins/mycelium/skills/canvas-healthManually: put the files in place
.devin/skills/canvas-health/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add haabe/mycelium --skill canvas-health -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about canvas-health
How to install canvas-health in Claude Code?
Run this command in your terminal: npx skills add haabe/mycelium --skill canvas-health -a claude-code.
What does canvas-health do?
It audits canvas knowledge base files for stale decay dates, missing mandatory documents, invalid evidence types, and contradictory confidence rationale.
Can canvas-health run without the Mycelium framework?
It runs, but in standalone mode it skips canvas state checks and theory gates that depend on Mycelium.
Source text
SKILL.md · MIT
---
name: canvas-health
description: "Lint canvas files for staleness, missing fields, inconsistent evidence types, and orphaned references. Run periodically or before major transitions."
metadata:
instruction_budget: "87"
framework_dependency: "mycelium"
framework_dependency_note: "This skill is designed to run within the Mycelium framework (https://github.com/haabe/mycelium). Standalone use will skip the canvas state, theory gates, and harness behavior the skill assumes. Install: /plugin install mycelium@haabe-mycelium."
---
# Canvas Health Check
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 records a decision (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", **or declaring the file an append-only log with no quality bar to
validate against**). Added v0.90.0; the append-only form added v0.234.0.
**WHY THE APPEND-ONLY FORM WAS MISSING FOR SO LONG, AND WHY IT IS THE SAME RULE.**
Step 9b below already exempts append-only logs from its length budget, on the
reasoning that for such a file the line count IS the value and pages-as-shape is a
category error. **The identical reasoning applies to a validation DATE on a file
with nothing to validate**, and this step never got the rule — so a dogfood
`archived-solutions.yml`, a registry of discarded leaves whose own `_meta` said
"no quality bar to validate against", was flagged as stale on every pass. Its author
had written the exemption in prose AND appended "flagged as missing `_meta` in
recurring canvas-health passes" to the marker, and it went on being flagged: **a
finding the author has pre-emptively annotated as noise is a check being routed
around**, which is the condition this very step warns trains a reader to skip it
(2026-09-21). Same one-rule-two-places shape as the horizon exemption written into
8c(b) and not carried to 8c(a). 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
7a. **Check that citations still resolve** (link rot, v0.205.0). Age is one axis of decay; a citation that 404s is the other, and until 0.205.0 nothing here asked. Run:
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_evidence_links.py" --project-dir .
```
It probes every http(s) URL cited under `.claude/canvas`, `diamonds`, `harness`, `memory`, `evals` and `docs/`, and writes a dated snapshot to `.claude/evals/metrics/evidence-links/`. It self-throttles to once per 14 days (`--force` to run anyway), so on most runs it prints "skipped" and the previous snapshot stands. Read the buckets, not the total:
- **ROTTED** (failed on two consecutive runs) → a warning naming the claim that cites it. Before deleting anything: the four found on 2026-08-17 had all MOVED, two under a different title. Search for the title before the URL.
- **pending** (first failing run) → info only; a single 404 can be a deploy blip. It is confirmed or cleared on the next run, never by a same-day re-run.
- **blocked** (401/403) → NOT rot; a human can open these. Those marked `needs_browser_check` are an ASK: open them in the browser and record the verdict with `--mark-verified <url> --verdict ok|gone|unclear`, so the check stops asking.
- **unknown** (429, 5xx, timeout, DNS/TLS) → not a finding in either direction; a large count means the run was throttled, not the evidence base rotting.
The framework's own citations are not the consumer's to check and are never scanned (a dead link in a skill doc is one upstream defect, not N project findings).
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.
8. **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)
7e. **Check that Cynefin classifications have not gone stale:**
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_cynefin_freshness.py" --project-dir .
```
The Cynefin gate is Required at exactly ONE decision — `start_experiment` — so a domain is
classified once per diamond and nothing re-runs it. **That matters because the domain drives
METHOD selection**: Complicated routes to expert analysis, Complex to probe-sense-respond. A
node classified on three interviews and never revisited after twenty more is routed by the
wrong method, confidently, on current data.
**Read UNCHECKABLE as a finding, not a pass.** A classification with no `cynefin_classified_at`
cannot be compared to anything. Measured on the dogfood canvas at adoption: **75
classifications, 0 checkable.** Baseline-gated for exactly that reason — failing all 75 would
force backfilling dates nobody knows, and inventing a classification date is the fabrication
this framework refuses elsewhere. Run `--write-baseline` ONCE; a classification written
afterwards must carry its date.
**A STALE row is a prompt to re-run `/mycelium:cynefin-classify`, never a verdict that the old
domain was wrong.** Evidence can land and leave a classification correct. What is not
acceptable is routing work by a classification that has never seen the data.
7d. **Check that stored ICE aggregates use the algorithm the framework documents:**
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_ice_aggregate.py" --project-dir .
```
**The rule since 0.319.0: a stored aggregate is the PRODUCT of its factors.** `ice-score` and
`docs/errata.md` A7 say ICE is Impact x Confidence x Ease, as Gilad defines it (*Evidence-Guided*,
ch. 3: *"simply the three values multiplied together"*). From v0.238.0 to 0.318.x this step
checked the opposite, that an aggregate was the **average** (Ellis, *Hacking Growth*: *"those
ratings are averaged"*; errata A1), because that was the documented rule then.
Why the check exists at all, unchanged: A1 told consumers to *"re-check decisions that turned on
them"*, and until v0.238.0 nothing checked whether anyone did. Measured on the dogfood canvas the
day this step was written: **14 of 14 verifiable aggregates were products, zero were averages**,
three releases after the erratum shipped.
**It is baseline-gated on purpose, and the baseline is not a snooze.** The aggregates that are
not the product when the baseline is taken are recorded, each with its value, and the check fails
only on one written or changed afterwards. An average on a shipped or discarded leaf (written
from 0.229.0 to 0.318.x) is the honest record of how that call was actually made; re-scoring it
now would only make a closed decision look compliant, which is the backfill
`check_leaf_lifecycle` refuses in plain words. **The check never rewrites canvas data, and you
must not either.**
**Read the exit and the first word of the verdict:**
- `OK` (exit 0): nothing that is not the product has appeared or changed since the baseline.
- `FAIL` (exit 1): a new or changed aggregate is not the product of its factors. Fix that score
with `/mycelium:ice-score`. **Adding a new failure to the baseline is the one use of
`--write-baseline` that is wrong** — it records a fresh mistake as history.
- `CANNOT ASSESS` (exit 2): aggregates that are not the product exist, and no baseline taken
under the 0.319.0 rule says which were already there. This is what a project sees on its
first run after upgrading, because a baseline written before 0.319.0 lists the old products
and not the averages. It is not a failure of those scores and it is not a pass. If they were
written before the project took 0.319.0, run `--write-baseline` ONCE (the earlier list is
carried over in the file as a record) and commit the baseline. If any was written after, fix
that score first.
**Read the UNVERIFIABLE bucket, it is not a pass.** An aggregate stored without its factors
cannot be recomputed at all; the check says so rather than counting it clean.
**What a green does NOT mean.** It checks one arithmetic identity. Whether Impact, Confidence and
Ease were honestly assessed is invisible from here — and a nearly-constant Ease column across a
corpus (8 of 15 at exactly 5 on the dogfood canvas) is the usual sign that one dimension is
carrying no information and ICE has quietly become a two-factor score.
7c. **Run the do-not-cite register scan** — do NOT re-derive this from prose:
```bash
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
8b2. **Check the entry locks** (v0.245.0). Run
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/scale_locks.py" --check`. Each open diamond is reported
as `holds`, `overridden by the user` (a well-formed line for it in `.claude/state/scale-lock-ack`) or `LOCKED`,
with what its parent has not yet established. A LOCKED diamond is a **warning**, and the finding
is the missing artefact, not the diamond: produce the purpose, the desired outcome, the
opportunity's evidence or the L3's confidence it names. Diamonds opened before v0.245.0 are
judged by the same locks; editing them is never blocked, but a LOCKED delivery diamond cannot
carry new source files (the discovery gate reads the same chain). Report any
`scale-lock-ack line ignored` line too: an override the user meant and the lock cannot read. The locks, their
sources and why they are artefacts at L1-L3 and a confidence band at L4-L5: `engine/diamond-rules.md`,
Entry locks.
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):
- **8c(e), added v0.182.0: every evidence-bearing source lands or is an orphan.** Run
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_evidence_landing.py" --project-dir .` It reports every
task with inbound touches or findings, every assumption-test and report file, and every user need
as LANDED / CLAIMED_NOT_LANDED / ORPHAN / REVIEWED, and fails only on regression against the
baseline the project committed with `--write-baseline`. 8c(b) and 8c(d) see two thirds of the
routing problem from the task side; this sees it from the source side, for every source class.
A CLAIMED_NOT_LANDED row is a write that reported success and did not happen: fix it first.
- **8c(f), added v0.183.0: an open opportunity that nothing reads.** Run
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_idle_opportunities.py" --project-dir .` It lists
every open opportunity with no open task naming it, no leaf carrying a live assumption with a
named test, and no `what_would_move_it`, as IDLE with its age, WARN tier. Measured the day it
was written: 50 of 66 open nodes on the dogfood canvas, 33 with no leaf at all. **Silence is
routing, not evidence**: four of seven discards recommended that day had to be withdrawn because
no task had ever been pointed at the node. The remedy is a reader (a task, a test, a declared
line), a park with a resume condition, or a merge; discard only on the record. `--strict` exits
1 for projects that have cleared the backlog.
- **8c(g), added v0.184.0: does anything follow an advisory.** Run
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/advisory_ledger.py" report --project-dir .` It prints,
per session-start advisory, how many sessions it was seen, how often the condition cleared by the
next session, how often it was still firing, the clear rate, and the current firing streak in
distinct days; and which advisories are muted, since when, and under what ruling. Report the
streaks at or past the mute threshold and the clear rate of anything that has fired more than
five times; cite these numbers under Measurement in `/bvssh-check`, which rated that dimension
amber three assessments running on advisories nobody counted. A muted advisory is a request for a
ruling: `advisory_ledger.py rule --id <id> --ruling keep|fix|drop --note "..."`. The ledger is
`.claude/state/advisory-ledger.jsonl`, append-only. N/A with no ledger is a project that has not
opened a session on 0.184.0 yet, not a clean bill.
- **8c(h), added v0.185.0: did the harness deliver our hooks at all.** Run
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_hook_delivery.py" --project-dir . --days 30`. It
reads the harness's own transcripts for this project (the only record of a hook being
cancelled) and reports, per hook, how many runs succeeded and how many were cancelled on
timeout. FAIL when the SessionStart hook was cancelled: a cancelled hook delivers NOTHING, the
operating contract included, and every check in this skill that also runs at session start
ran nowhere. Measured on the dogfood repo 2026-09-10: 32 of 33 session starts in a month were
cancelled at the manifest's own 5-second timeout while every test stayed green, because tests
run the script and not the harness. Report this line FIRST; every other finding in this pass
is conditional on it. N/A on a runtime that keeps no transcripts.
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.
**Unless the task carries a `frozen_prediction` whose horizon has passed** (v0.207.0): then say "due for scoring", offer none of the three, and point at `check_instrument_contract.py`, which lists it under PREDICTIONS IN THE CANVAS. A due prediction surfaced as a cold task is a finding pointed at the wrong action, and on 2026-09-03 the task's own `do_not` forbade the nudge this line would have offered.
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. Ad...
© haabe · MIT · haabe/mycelium