no0dles
## What to build Introduce the read-only **state-key diff engine** that powers cache explainability, plus the `hammerkit explain [task]` command. Today the scheduler computes a per-task **state key**, compares it to the stored one to decide hit/miss, then discards the comparison. This slice keeps that comparison as a structured diff and persists, **per task name** (not per task id), a "last-resolved record" — the task id, its description, and its source stats — updated each run. Because the record is keyed by task *name*, `explain` can still diff against the previous run even when a definition change moves the **task id**. The record is additive metadata and MUST NOT influence the hit/miss decision (which stays `id` + `stateKey`). `hammerkit explain [task]` reports, for each in-scope task and its dependencies, whether it would be a cache hit or miss — **executing no command, starting no container/service, and performing no cache push/pull**. For a miss it classifies the cause (source changed/added/removed, env var changed, command changed, image changed, dependency changed, or never cached) and names the concrete identifier (file path, env var name, dependency task id). Multiple simultaneous causes are all reported. All output routes through the `Environment` (no `console.*`). This is the **keystone**: the inline run miss-reason, the summary cause column, and `run --dry-run` all reuse this engine rather than reimplementing it. References: `specs/cache-explain/spec.md` (US1, FR-001–004, 007, 008); glossary **state key** / **task id** in `CONTEXT.md`; cache identity per `docs/adr/0002`. ## Acceptance criteria - [ ] `hammerkit explain <task>` reports each in-scope task + its deps as a predicted cache hit or miss, executing zero commands and starting zero containers/services (SC-002). - [ ] After a cached run, changing one source file makes `explain` report a miss whose cause names that exact file (SC-001). - [ ] A never-run task is explained as "never cached", not an error (SC-003). - [ ] When only an upstream dependency changed, the downstream task's cause is the dependency, not its own sources (SC-004). - [ ] A per-task-name last-resolved record (task id + description + source stats) is persisted and updated each run and does not change the hit/miss decision. - [ ] A cache entry written by an older hammerkit (no component breakdown) degrades to cause "cache format changed" rather than crashing. - [ ] A task with no `src` is reported as uncacheable with cause "no src declared". - [ ] Multiple changed inputs report all causes, not just the first. - [ ] All output routes through the `Environment`. ## Blocked by - None - can start immediately.