@journeykit/canon 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ # canon-report-dashboard
2
+
3
+ canon's single status surface: `canon report` renders TEN panels to
4
+ markdown, `canon report --snapshot` exports the same ten marts to
5
+ Parquet, and `canon dashboard` serves a small zero-network web app that
6
+ renders SEVEN of them from such a snapshot. Neither surface computes an
7
+ aggregate itself — both read canon's precomputed
8
+ `crates/canon-store/sql/views.sql` marts, and each run reads the corpus
9
+ ONCE (`crates/canon-report/src/query.rs`'s `PIN_CORPUS_SQL`), so no two
10
+ panels within one report, or two files within one snapshot, can
11
+ disagree. Across surfaces the input is what differs: a dashboard renders
12
+ the snapshot it was given, so it matches a report only insofar as the
13
+ two were generated from the same corpus.
14
+
15
+ They can still disagree about what a number MEANS, and that is the
16
+ failure this skill is written against: a friendly label or a paraphrase
17
+ asserting something its query never computed. Every claim below is kept
18
+ verbatim with the prose `canon report` actually renders
19
+ (`canon_report::render`'s `pub const *_PANEL` literals and its inline
20
+ one-liners), and `packages/dashboard/test/panel-copy.test.ts` pins this
21
+ file to those literals. Copy a claim; do not paraphrase it.
22
+
23
+ ## Generated, never hand-edited
24
+
25
+ `.canon/REPORT.md` carries a leading `<!-- GENERATED by \`canon report\` …
26
+ DO NOT EDIT. -->` comment. If it looks stale or wrong, the fix is
27
+ **always** re-running `canon report` (or fixing the underlying
28
+ ledger/policy it renders) — never hand-editing the file. The report
29
+ header embeds only input-derived digests (corpus hash, policy hash,
30
+ ledger-head hash) and **no timestamp, no `source_git_sha`**, so a
31
+ committed report is reproducible from named inputs and `canon report
32
+ --check` can byte-diff it deterministically. `source_git_sha` belongs
33
+ only in `--snapshot`'s `manifest.json` (below), which is never
34
+ drift-checked.
35
+
36
+ ## `canon report [--repo <dir>]`
37
+
38
+ Renders the current ten-panel report to `<repo>/.canon/REPORT.md`.
39
+ `--repo` resolves through the nearest-`canon.yaml`-ancestor walk every
40
+ subcommand uses — omit it (or pass `.`) to run from any subdirectory.
41
+
42
+ ```bash
43
+ canon report # writes .canon/REPORT.md
44
+ canon report --repo /path/to/repo
45
+ ```
46
+
47
+ Between the digest header and the first panel, the report may also
48
+ render a `## Kinds not read directly` section. It is config-derived
49
+ (`canon.yaml`'s static `routing`/`tiers` tables), not a live backend
50
+ probe, and an empty set renders NO section at all — so a repo routing
51
+ nothing to a not-directly-readable backend renders exactly as before
52
+ the section existed.
53
+
54
+ ## `canon report --check` — the CI drift gate
55
+
56
+ Regenerates the report in memory and byte-compares it against the
57
+ committed `.canon/REPORT.md`:
58
+
59
+ - `MISSING` (exit 1) — no report has ever been written; run `canon report` first.
60
+ - no drift (exit 0) — the committed file is byte-identical to what the
61
+ current inputs would render now.
62
+ - `DRIFT` (exit 1) — the committed file disagrees with current inputs
63
+ (hand-edited, or the corpus/policy/ledger moved and nobody re-ran
64
+ `canon report`).
65
+
66
+ ```bash
67
+ canon report --check # wire into CI exactly like canon format --check / canon gate check
68
+ ```
69
+
70
+ Wire it as a CI step or pre-push hook the same way `canon gate
71
+ install-hooks` wires `canon gate check`; run `canon report --check`
72
+ directly wherever CI already runs those.
73
+
74
+ Exports all ten views to `<dir>/<table>.parquet` plus a
75
+ `<dir>/manifest.json`. One explicit `COPY` per view — the exported
76
+ columns are each view's own `SELECT` list, wider than the curated column
77
+ subset the markdown tables render. `file` is always
78
+ `<table>.parquet`, and the table order is the report's own panel order,
79
+ with `mart_scope_status`, `mart_subjects`, `mart_review_rounds`,
80
+ `mart_review_totals` and `mart_run_observability` APPENDED last so an
81
+ existing consumer's order never moves:
82
+
83
+ ```json
84
+ {
85
+ "generated_at": "...",
86
+ "source_git_sha": "...",
87
+ "source_digest": "...",
88
+ "tables": [
89
+ { "table": "mart_trust_matrix", "file": "mart_trust_matrix.parquet" },
90
+ { "table": "mart_session_costs", "file": "mart_session_costs.parquet" },
91
+ { "table": "mart_role_memory", "file": "mart_role_memory.parquet" },
92
+ { "table": "mart_flywheel_funnel", "file": "mart_flywheel_funnel.parquet" },
93
+ { "table": "mart_review_burndown", "file": "mart_review_burndown.parquet" },
94
+ { "table": "mart_scope_status", "file": "mart_scope_status.parquet" },
95
+ { "table": "mart_subjects", "file": "mart_subjects.parquet" },
96
+ { "table": "mart_review_rounds", "file": "mart_review_rounds.parquet" },
97
+ { "table": "mart_review_totals", "file": "mart_review_totals.parquet" },
98
+ { "table": "mart_run_observability", "file": "mart_run_observability.parquet" }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ This is where `source_git_sha`/`generated_at` live (never in the
104
+ drift-checked markdown header) — a snapshot is output-inclusive
105
+ provenance. `source_digest` is one 12-hex fingerprint over the same
106
+ corpus/policy/ledger digests the report header renders, so a snapshot's
107
+ provenance is comparable to a report's without re-deriving anything.
108
+
109
+ ```bash
110
+ canon report --snapshot /tmp/canon-snap
111
+ ```
112
+
113
+ ## `canon dashboard [--repo <dir>] [--snapshot <dir>] [--port <n>]`
114
+
115
+ Serves the dashboard web app locally, pointed at a real snapshot:
116
+
117
+ ```bash
118
+ # build the app once
119
+ bun install && bun run --cwd packages/dashboard build
120
+
121
+ canon dashboard # regenerates a fresh snapshot every run, serves it
122
+ canon dashboard --snapshot /tmp/canon-snap --port 8080
123
+ ```
124
+
125
+ - `--snapshot` omitted: `canon dashboard` calls `canon report --snapshot`
126
+ itself every run, into the conventional `.canon/dashboard-snapshot`
127
+ scratch directory — always fresh.
128
+ - `--snapshot <dir>` given: served AS-IS when `<dir>` already has a
129
+ `manifest.json` (a separately generated snapshot, or a downloaded CI
130
+ artifact) — never silently regenerated over it. If it has no
131
+ `manifest.json`, one is generated there once.
132
+ - Prints the URL to open, then serves until Ctrl-C. If the app is not
133
+ built, `canon dashboard` fails with the exact build command to run.
134
+
135
+ The dashboard renders SEVEN panels — trust matrix, session costs, role
136
+ memory, flywheel funnel, review burn-down, review rounds, review totals.
137
+ **Scope status
138
+ and subjects are exported to the snapshot but have no dashboard panel**;
139
+ for those two, read `.canon/REPORT.md` or query the Parquet directly.
140
+ Do not read a missing dashboard panel as an empty mart.
141
+
142
+ The dashboard's freshness banner shows the snapshot's
143
+ `generated_at`/`source_git_sha`/`source_digest` with a "scoped to this
144
+ snapshot, not necessarily the live checkout" note — treat "matches
145
+ `canon report`" as "matches the inputs that produced THIS snapshot".
146
+
147
+ ## Reading the ten panels
148
+
149
+ In `canon report`'s own render order. Each panel names the view that
150
+ computes it; nothing here is derived a second time.
151
+
152
+ - **Trust matrix** (`mart_trust_matrix`) — Change/task coverage × green × who.
153
+ `covered` means at least one `evidence_record` exists for this `task_id`,
154
+ NOT that policy's REQUIRED evidence is present — that is `canon gate
155
+ check`'s question, and this view deliberately does not answer it. `green`
156
+ is the plain `verdict = 'faithful'` proxy over the latest attestation, not
157
+ `canon-gate`'s multi-rung trust-ladder classifier, which the view refuses
158
+ to re-implement in SQL. `evidence_count` counts ATTESTATIONS, superseded
159
+ versions included, because a re-attestation can only arrive as a new
160
+ version; `green` and `who` are read off one whole winner row, so they can
161
+ never come from two different attestations.
162
+
163
+ - **Session costs** (`mart_session_costs`) — token/cost per
164
+ `(session, client, role, workspace_label)`. Every `run`/`session`/
165
+ `token_usage` record is folded to its latest version first, so
166
+ a re-ingested corrected cost REPLACES the superseded figure instead of being summed with it.
167
+ `workspace_label` is NOT a repo identity: it is the last non-empty path
168
+ segment of the `token_usage` event's workspace key and nothing more, so it
169
+ SPLITS one repo whose worktrees sit in differently-named directories and
170
+ MERGES two different repos sharing a directory name. Two stronger fields
171
+ exist and this mart reads neither — the event's own `workspace_key`, and
172
+ `Session.project_key`, which `canon-cli` stamps to the main worktree's key.
173
+
174
+ - **Role memory** (`mart_role_memory`) — per-`(role, regime_key)` strategy
175
+ counts. `hit_rate` is NOT a retrieval hit rate: it is the fraction of that
176
+ namespace's distilled strategies carrying no `demotion` flag, i.e.
177
+ exactly `active_count / strategy_count`.
178
+ `avg_source_trajectories` is the mean number of source trajectories a
179
+ strategy was distilled from — an explicitly-named stand-in, because
180
+ canon records no per-strategy reward or effect metric.
181
+ There is no effect column, and this panel measures no effect.
182
+
183
+ - **Flywheel funnel** (`mart_flywheel_funnel`) — verdicts → distilled →
184
+ retrieved → applied, per role, where
185
+ the last three stages count STRATEGIES, so the funnel narrows by construction.
186
+ `applied` is the retrieved set narrowed to strategies whose recipient run
187
+ has something recorded about how it ended, split into
188
+ `applied_attributed` (a resolved trajectory of the same role stamped with
189
+ that run id) and `applied_proxy` (only the run's own terminal status).
190
+ Both rules are CO-OCCURRENCE inside one run, never causation:
191
+ canon stores no edge from a strategy to a verdict,
192
+ so neither column can separate guidance that was followed from guidance
193
+ that was ignored. A large drop between two stages is the actionable
194
+ signal; `retrieved 0` means no run's recorded guidance names a strategy
195
+ that exists now.
196
+
197
+ - **Review burn-down** (`mart_review_burndown`) — a per-day trend over raw
198
+ `Divergence.status` events, with the `evidence_*` columns breaking the same
199
+ window down by `EvidenceRecord.verdict`. `divergence_open_running_total` is
200
+ a running `opened - resolved` event count, NOT the number open now —
201
+ the two differ whenever one `resolved` record closes several findings on a
202
+ scenario. For current state per scenario, run `canon divergence status`.
203
+
204
+ - **Scope status** (`mart_scope_status`) —
205
+ Every specified scenario × evidence-verified × plan-carried × spec-covered (`mart_scope_status`). Driven by the spec corpus, so a scenario no plan task declares still appears — with a NULL `task_id`, which is the worklist entry.
206
+ Grain is one row per authored `(project_id, scenario_id)`; the driving
207
+ side carries the project, so two spec roots authoring one scenario id
208
+ are two rows by construction. `evidence_covered`/`green` count either
209
+ side — the task the scenario is declared by, or scenario-keyed evidence
210
+ authored directly against it (`scenario_verdict`). `spec_covered IS
211
+ NULL` means NO coverage overlay row exists for that scenario — a
212
+ different state from an overlay row that exists and says
213
+ `spec_covered = false`. Read-only reporting; never a `canon gate` input.
214
+
215
+ - **Subjects** (`mart_subjects`) —
216
+ Per-domain subject rollup: status × scenario coverage.
217
+ One row per `subject` record, folded to its latest version, so a subject
218
+ that walked proposed → shipped is still one row. `scenario_count` is how
219
+ many `scenario_ids` the subject links; `covered_scenarios` is how many of
220
+ those carry a latest NON-divergent evidence verdict (`faithful` or
221
+ `not_applicable`), the same last-wins fold the `verifying -> shipped` gate
222
+ uses. Read-only reporting; never a `canon gate` input. See `canon-subject`.
223
+
224
+ - **Review rounds** (`mart_review_rounds`) — one row per `(change_id, round)`
225
+ over `Finding` records (`canon finding add`, then `canon finding close` to
226
+ move a finding's disposition), folded to the latest version
227
+ of each `{change_id}__{round}__{seq}` finding first, so a finding raised
228
+ `open` and later closed is counted once and in one disposition bucket.
229
+ A round that found
230
+ nothing wrote no `Finding` and so has no row: the table
231
+ counts the rounds that FOUND something, never the rounds RUN
232
+ (s42's round 12 returned MERGEABLE with zero findings and is absent),
233
+ and canon has no record kind that marks a round as run.
234
+ `fix_of_fix` counts
235
+ findings whose SOURCED `introduced_by` equals the `resolution_sha` of a
236
+ finding earlier in the SAME change, ordered strictly by `(round, seq)`.
237
+ That is
238
+ a commit-id equality join over two recorded fields and nothing more:
239
+ it reads no git history, computes no blame,
240
+ and establishes no causal claim.
241
+ `introduced_by_unsourced` is the UNKNOWN bucket —
242
+ a finding with no sourced `introduced_by` is counted there and NEVER as
243
+ not-a-fix-of-fix.
244
+ What the count MEANS is one sentence, and every surface that reports it
245
+ states this sentence and no paraphrase of it:
246
+ `fix_of_fix` bounds NOTHING — not from below, not from above: it
247
+ UNDER-counts, because an unsourced finding is never counted and a fix in
248
+ one change that breaks something first found while reviewing a DIFFERENT
249
+ change is not counted at all; it OVER-counts, because a `resolution_sha`
250
+ commit may carry work BEYOND the fix and every finding recording that
251
+ commit is counted regardless; and for any individual match the data
252
+ cannot say whether the fix or the other work in that commit introduced
253
+ the defect.
254
+ Both directions are live in canon's own rows —
255
+ `f438c610` closed s42's round 8 AND shipped s42's whole feature, so
256
+ round 9's six matches cannot be attributed either way.
257
+ `reviewed_sha` is the greatest value any of
258
+ the round's findings recorded and reads `—` when none did; a round that
259
+ reviewed an uncommitted working tree is the usual reason, but the view
260
+ cannot distinguish that from findings that left the field unset.
261
+
262
+ - **Review totals** (`mart_review_totals`) — one row per `change_id`,
263
+ and every column a `sum()`, `count(*)` or `max()` over the
264
+ `mart_review_rounds` rows above. This is the panel a release note is
265
+ copied from: the per-round table left "N findings across R rounds" as
266
+ hand arithmetic, and hand arithmetic over a generated table is what
267
+ put four wrong figures into this release line's published notes, so
268
+ this panel exists so a release note is a COPY rather than a
269
+ computation. The view's only `FROM` is `mart_review_rounds`, and
270
+ every panel in one report — or one `--snapshot` export — is computed
271
+ in a single DuckDB process over one materialized read of the corpus,
272
+ so the total and the rows it totals are one computation over one
273
+ input and cannot disagree: a record written to the ledger mid-run
274
+ reaches BOTH tables or neither, never one and not the other — though the
275
+ digest header beside these panels is a SEPARATE read outside that pin,
276
+ taken before it in a report and after it in a `--snapshot`, so header
277
+ and panels can still describe the corpus a moment apart. (Both halves
278
+ matter. The SQL
279
+ argument alone held while `canon report` ran one `duckdb` process per
280
+ mart against a live ledger, and a finding written between two of them
281
+ reached one panel and not the other — s43 round 6's blocker.
282
+ `crates/canon-report/src/query.rs`'s `PIN_CORPUS_SQL` is the other
283
+ half.)
284
+ `rounds_recorded` is `count(*)` over those rows, and that view holds a
285
+ row only for a round that RECORDED a finding, so
286
+ this column counts the rounds that FOUND something, never the rounds RUN;
287
+ canon has no record kind for a review round, so the rounds-RUN number
288
+ is not derivable from this corpus at all. `highest_round` is
289
+ `max(round)`, and it witnesses nothing about rounds: above
290
+ `rounds_recorded` it says only that some round number below it has no
291
+ row, and canon cannot say why — `round` is author-supplied and
292
+ nothing requires a change's rounds to be numbered from 1 or without
293
+ gaps, so a change whose only finding is labelled round 7 shows the
294
+ same gap six silent rounds would. Neither the gap nor its absence
295
+ evidences a round that RAN. Neither column is the rounds-RUN count —
296
+ `highest_round` is a round NUMBER, not a count, and `round` is
297
+ author-supplied. The splits sum the per-round `FILTER` counts, so
298
+ `severity_* = findings`, `disposition_* = findings` and
299
+ `introduced_by_sourced + introduced_by_unsourced = findings` all hold
300
+ at this grain too. `fix_of_fix` is `sum(fix_of_fix)` over a column
301
+ nothing records, and it means exactly what it means one panel up:
302
+ `fix_of_fix` bounds NOTHING — not from below, not from above: it
303
+ UNDER-counts, because an unsourced finding is never counted and a fix in
304
+ one change that breaks something first found while reviewing a DIFFERENT
305
+ change is not counted at all; it OVER-counts, because a `resolution_sha`
306
+ commit may carry work BEYOND the fix and every finding recording that
307
+ commit is counted regardless; and for any individual match the data
308
+ cannot say whether the fix or the other work in that commit introduced
309
+ the defect.
310
+ There is no defect rate, no quality score and no comparison between changes here:
311
+ a row is one change's review history, and `findings` moves with how
312
+ much reviewing happened rather than with the change.
313
+
314
+ Two marts in `views.sql` are NOT report panels and are not exported by
315
+ `--snapshot`: `mart_records_by_kind` and `mart_session_run_handoff`.
316
+ Read them with DuckDB directly against `views.sql`.
317
+
318
+ - **Run observability** (`mart_run_observability`) — one row per folded
319
+ `run_id`, appended as the tenth report/snapshot table after the established
320
+ nine tables. It reports lifecycle identifiers/status/timestamps, latency
321
+ only when both timestamps exist, event and label counts, child-run count,
322
+ guidance count, lineage provider/model, and token/cost source totals. These
323
+ are bounded observations and proxies: event counts do not infer tool calls,
324
+ retries, or human interventions, and no field claims guidance caused an
325
+ outcome. `directive_event_count` and `guidance_count` are counts only;
326
+ directive and guidance text is never emitted. `*_unknown` columns explicitly
327
+ expose missing joins/coverage, including missing session/project/task/lineage,
328
+ end timestamps, event times, costs, and guidance. Workspace absolute paths
329
+ and verbatim directives/guidance are intentionally omitted. The adapter
330
+ contract is strict read-only validation; this panel does not execute
331
+ adapters or enforce provider sandbox capabilities. Privacy-safe omission of
332
+ paths and directive/guidance text is part of the current semantics.
333
+
334
+ ## What this skill does NOT cover
335
+
336
+ - The underlying view layer that computes the marts — see `canon-storage`.
337
+ - `canon gate`'s evidence-gated checkbox flip / trust ladder — see
338
+ `canon-gate`. The trust-matrix and scope-status panels READ the same
339
+ corpus `canon gate check` validates but never gate or flip anything.
340
+ - Strategy distillation/promotion — see `canon-learn`; the role-memory and
341
+ flywheel panels are read-only views over that store.
342
+ - Authoring subjects and their status lifecycle — see `canon-subject`; the
343
+ subjects panel is the read-only rollup over those records.
@@ -0,0 +1,123 @@
1
+ # canon-retrieve
2
+
3
+ `canon retrieve` surfaces a role's promoted strategy memory (see
4
+ `canon-learn`) at task-dispatch time. It is a fail-soft, advisory-only
5
+ read: it NEVER blocks a dispatch and NEVER gates reproducibility.
6
+
7
+ ## `canon retrieve --role <r> --regime <k> [--k <n>] [--repo <dir>] [--json]`
8
+
9
+ Prints the top-`k` retrievable strategies stored for `regime_key = <k>`
10
+ (active strategies plus backward-compatible legacy rows without a lifecycle;
11
+ guardrails are just strategies distilled from a failure-polarity verdict),
12
+ newest-distilled first, capped at `--k` (default 5). Demoted,
13
+ quarantined, rejected, and rolled-back rows are not retrievable.
14
+
15
+ ```bash
16
+ canon retrieve --role dev --regime dev/canon/join-spine/9c93d024b1a2
17
+ canon retrieve --role dev --regime dev/canon/join-spine/9c93d024b1a2 --k 3 --json
18
+ ```
19
+
20
+ - `--regime` is the FULL `regime_key` string
21
+ (`<role>/<repo>/<area>/<hash>`) — the same serialization the write side
22
+ produces.
23
+ - Assembling `--regime` in a script or hook? Use `canon regime-key
24
+ --role <r> --repo <r> --area <a> --hash <h>`: it runs canon's ONE
25
+ canonical `regime_key` serializer and prints the validated key, so a
26
+ caller routes through the exact normalizer the write path uses instead
27
+ of a drifting shell slug. Exits `2` with empty stdout on a malformed
28
+ result (empty segment / bad `<hash>`) — a clean fail-soft branch for
29
+ `|| exit 0` callers.
30
+ - `--role` MUST equal `--regime`'s leading segment — a mismatch is a
31
+ clean usage error, exit `2` (mirroring `canon gate check`'s
32
+ 0-clean/1-red/2-usage convention), never a panic or a silently-ignored
33
+ mismatch.
34
+ - `--repo` resolves through the same nearest-`canon.yaml`-ancestor walk
35
+ `canon context`/`canon format`/`canon gate` use. The resolved
36
+ `canon.yaml` `learn:` section names the store root (`.canon/learn` by
37
+ default); a repo with no `canon.yaml` still works, defaulting cleanly.
38
+
39
+ **FAIL-SOFT, always exit `0`** once `--role`/`--regime` agree: a store
40
+ outage, an empty/nonexistent store, or a malformed on-disk row all
41
+ degrade to an empty guidance list — never a nonzero exit or an error a
42
+ caller must handle.
43
+
44
+ ## The pre-dispatch hook (`scripts/canon-retrieve-pre-dispatch.sh`)
45
+
46
+ A generic, fail-soft, advisory-only shell script that fires at a
47
+ PreToolUse-equivalent point for a `task`-shaped dispatch (Claude Code's
48
+ `Task` tool / Codex's equivalent). It reads the tool's `subagent_type`
49
+ as `--role`, assembles `--regime` by routing its repo/area segments
50
+ through `canon regime-key` (so it never re-derives the join key with a
51
+ drifting shell slug), calls `canon retrieve --json`, and emits the
52
+ result as a `PreToolUse` `hookSpecificOutput.additionalContext` JSON
53
+ envelope on stdout — Claude Code's convention for surfacing advisory
54
+ context without a permission decision. It NEVER blocks: a missing
55
+ `canon`/`jq`, a non-`Task` tool call, or empty guidance are all silent
56
+ no-ops, exit `0`.
57
+
58
+ Segment sources and override env vars
59
+ (`CANON_RETRIEVE_ROLE`/`CANON_RETRIEVE_AREA`/`CANON_RETRIEVE_HASH`/
60
+ `CANON_RETRIEVE_REGIME`) are documented in the script's own header.
61
+
62
+ ### Wiring it
63
+
64
+ Place the script wherever your repo keeps materialized hook scripts
65
+ (e.g. `.claude/hooks/canon-retrieve-pre-dispatch.sh`), then register the
66
+ hook-seam entry with `canon gate install-hooks`:
67
+
68
+ ```bash
69
+ canon gate install-hooks --repo . --event PreToolUse \
70
+ --matcher Task --command "sh .claude/hooks/canon-retrieve-pre-dispatch.sh" \
71
+ --timeout 15
72
+ ```
73
+
74
+ This merges the standard `{matcher, hooks: [{type: "command", command,
75
+ timeout}]}` entry into BOTH `<repo>/.claude/settings.json` and
76
+ `<repo>/.codex/hooks.json`, additive and idempotent — see the
77
+ `canon-gate` skill for `install-hooks`'s full contract.
78
+
79
+ ## Replay vs. live retrieval
80
+
81
+ A run manifest records `injected_guidance` — a full SNAPSHOT (each entry
82
+ `{strategy_id, title, content}`) of what `canon retrieve` returned at
83
+ dispatch time, visible in `--json` output. It is never a live pointer, so
84
+ a later edit or demotion of the source strategy can never retroactively
85
+ change what a manifest already recorded.
86
+
87
+ - **New run:** dispatch calls `canon retrieve` (live), then records the
88
+ result into the run manifest's `injected_guidance`.
89
+ - **Replay:** reuse the manifest's recorded `injected_guidance` UNCHANGED
90
+ — never a fresh `canon retrieve` call. A replay MUST read the stored
91
+ guidance, so a live store change after the original run (a strategy
92
+ added, edited, demoted, or removed) can never perturb the replay.
93
+
94
+ `canon retrieve` and the pre-dispatch script work standalone today — a
95
+ caller can print retrieved guidance with zero manifest integration.
96
+
97
+ ## Lifecycle and legacy limitations
98
+
99
+ Retrieval is a read path, not an activation or evidence path. It serves only
100
+ exact role/regime matches and excludes `quarantined`, `rejected`,
101
+ `rolled_back`, and demoted strategies. Newly distilled strategies are
102
+ quarantined by default and therefore do not appear until the paired evaluation
103
+ and signed approval workflow in `canon-learn` activates them.
104
+
105
+ Rows written before lifecycle tracking (`lifecycle: null`) remain readable for
106
+ backward compatibility when they are not demoted. They have no current
107
+ quarantine, paired-evaluation, or SSH-approval provenance; retrieval MUST NOT
108
+ interpret them as proof of the signed workflow, upgrade them, or claim
109
+ measured uplift. The repository currently has no published measured uplift
110
+ result.
111
+
112
+ Role/regime retrieval is exact and advisory, not similarity search or a
113
+ quality guarantee. A live lookup is also not a replayable proof: dispatch
114
+ manifests snapshot the returned `{strategy_id, title, content}` entries, and
115
+ replay uses that snapshot rather than refreshing the store.
116
+
117
+ ## What this skill does NOT cover
118
+
119
+ - Strategy distillation/promotion/demotion — see the `canon-learn`
120
+ skill.
121
+ - `canon gate install-hooks`'s full contract — see the
122
+ `canon-gate` skill; this skill documents only the one entry it
123
+ installs for `canon retrieve`.