@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.
- package/dist/index.js +12 -2
- package/dist/skills/SKILL.src.md +46 -0
- package/dist/skills/reference/canon-artifact-ingest.md +72 -0
- package/dist/skills/reference/canon-authoring.md +155 -0
- package/dist/skills/reference/canon-context.md +137 -0
- package/dist/skills/reference/canon-fmt.md +43 -0
- package/dist/skills/reference/canon-gate.md +199 -0
- package/dist/skills/reference/canon-inventory.md +102 -0
- package/dist/skills/reference/canon-learn.md +245 -0
- package/dist/skills/reference/canon-plan-import.md +143 -0
- package/dist/skills/reference/canon-plugins.md +131 -0
- package/dist/skills/reference/canon-policy.md +119 -0
- package/dist/skills/reference/canon-report-dashboard.md +343 -0
- package/dist/skills/reference/canon-retrieve.md +123 -0
- package/dist/skills/reference/canon-session-ingest.md +214 -0
- package/dist/skills/reference/canon-storage.md +152 -0
- package/dist/skills/reference/canon-subject.md +161 -0
- package/dist/skills/reference/canon-vocab.md +115 -0
- package/dist/skills/scripts/canon-retrieve-pre-dispatch.sh +126 -0
- package/package.json +6 -4
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# canon-gate
|
|
2
|
+
|
|
3
|
+
`canon gate` is canon's trust spine: a two-layer evidence gate over a
|
|
4
|
+
repo's artifact corpus. It answers two DIFFERENT questions per artifact —
|
|
5
|
+
"does required evidence exist" (coverage) and "did the evidence pass, by
|
|
6
|
+
whom, how stale" (verdict-ledger) — never collapsing them into one
|
|
7
|
+
"done" boolean.
|
|
8
|
+
|
|
9
|
+
## The trust ladder
|
|
10
|
+
|
|
11
|
+
Each artifact climbs a trust ladder as evidence accrues: an unreviewed
|
|
12
|
+
record is weaker than a reviewed one, which is weaker than a ratified
|
|
13
|
+
one. `policy.yaml` sets a `trust_required` level per artifact class; the
|
|
14
|
+
release-scoped check (below) enforces it. A human-only `flagged` overlay
|
|
15
|
+
overrides the ladder — a flagged artifact is never green regardless of
|
|
16
|
+
evidence. (Author `policy.yaml`'s trust/staleness fields via `canon-policy`.)
|
|
17
|
+
|
|
18
|
+
## The eight failure classes
|
|
19
|
+
|
|
20
|
+
Every violation carries one of these stable, grep-able strings:
|
|
21
|
+
|
|
22
|
+
| Class | Meaning |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. |
|
|
25
|
+
| `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record. |
|
|
26
|
+
| `trust-below-required` | Achieved trust level is below `policy.yaml`'s `trust_required` for its class — RELEASE-scoped only (`canon gate check --release`); never fires on an ordinary run. |
|
|
27
|
+
| `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. |
|
|
28
|
+
| `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
|
|
29
|
+
| `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
|
|
30
|
+
| `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
|
|
31
|
+
| `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) or a bare `verified` claim with no attached command result. |
|
|
32
|
+
|
|
33
|
+
## `canon gate check [--repo <dir>] [--release]`
|
|
34
|
+
|
|
35
|
+
Assembles coverage + ledger + staleness + the always-on trust-ladder
|
|
36
|
+
check, plus the opt-in spec-coverage check, over the resolved repo's
|
|
37
|
+
corpus and runs them, printing every violation grouped by failure class.
|
|
38
|
+
`--release` additionally engages the release-scoped
|
|
39
|
+
`trust-below-required` check; the trust-ladder check is never dropped
|
|
40
|
+
when `--release` is passed.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
canon gate check --repo . # ordinary evaluation
|
|
44
|
+
canon gate check --repo . --release # + trust-below-required
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exit `0` clean, `1` gate-red (any violation), `2` usage/load failure
|
|
48
|
+
(unreadable ledger, corrupt `canon.yaml`). `--repo` (or its omission)
|
|
49
|
+
resolves through the nearest-ancestor `canon.yaml` walk, so it reads the
|
|
50
|
+
repo ROOT's `.canon/policy.yaml` and `.canon/ledger` from any subdirectory.
|
|
51
|
+
|
|
52
|
+
### The spec-derived worklist (`spec_coverage`, opt-in)
|
|
53
|
+
|
|
54
|
+
Coverage groups the EVIDENCE corpus, so it can only ever enumerate
|
|
55
|
+
artifacts that already have evidence. A `.feature` scenario nobody has
|
|
56
|
+
attested to appears in no group and is invisible to it. `spec_coverage`
|
|
57
|
+
adds the other direction: it starts from the Scenario corpus
|
|
58
|
+
`canon inventory sync` materializes and left-joins evidence onto it, so
|
|
59
|
+
an unimplemented or mismatched spec becomes a reported violation.
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
# .canon/policy.yaml — ABSENT by default; absent means zero violations
|
|
63
|
+
spec_coverage:
|
|
64
|
+
require_evidence: true
|
|
65
|
+
scope: [building, verifying] # optional; omit for the whole corpus
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Unimplemented** — no `EvidenceRecord` carries the scenario's
|
|
69
|
+
`(project_id, scenario_id)`. Author one with
|
|
70
|
+
`canon evidence add --scenario-id <id> --project-id <root-id>`; both
|
|
71
|
+
flags are required together, because two spec roots may carry the same
|
|
72
|
+
scenario id.
|
|
73
|
+
- **Mismatched** — the folded divergence state is `open`,
|
|
74
|
+
`still-divergent`, or `resolved-invalid` (a resolution whose app sha
|
|
75
|
+
has moved), or the latest ledger verdict is `divergent`.
|
|
76
|
+
- **`scope`** narrows blocking to scenarios whose `@subject:<id>` tag
|
|
77
|
+
links them to a Subject in one of the named statuses. With a `scope`
|
|
78
|
+
set, an untagged scenario is out of scope; with `scope` omitted, every
|
|
79
|
+
scenario is in scope. A tag naming no Subject record is reported as a
|
|
80
|
+
dangling link, never silently skipped.
|
|
81
|
+
|
|
82
|
+
Two refusals rather than a silent pass, both surfacing as
|
|
83
|
+
`uncovered-cell` on the subject `spec_coverage`:
|
|
84
|
+
|
|
85
|
+
- A **malformed section** (unknown key, unknown `scope` status) refuses
|
|
86
|
+
at check time. It is never treated as absent — a typo must not be
|
|
87
|
+
silently equivalent to not opting in.
|
|
88
|
+
- A **corpus kind routed off the gate's rung** (`scenario`,
|
|
89
|
+
`divergence`, or `subject` sent anywhere but `local`) refuses, because
|
|
90
|
+
the gate reads one tier and would otherwise pass by seeing nothing.
|
|
91
|
+
|
|
92
|
+
Malformed rows in those three kinds do NOT surface as
|
|
93
|
+
`malformed-evidence`; they are kept off the evidence violation set on
|
|
94
|
+
purpose, so enabling nothing changes no existing verdict.
|
|
95
|
+
|
|
96
|
+
## Effect-aware risk approvals (`risk_tiers`)
|
|
97
|
+
|
|
98
|
+
`risk_tiers` is absent by default and remains a no-op when absent. When it
|
|
99
|
+
is present, `canon gate check` selects the highest-ranked configured tier
|
|
100
|
+
whose current evidence binding matches a path or exact `effect:<kebab-slug>`.
|
|
101
|
+
The current generation is selected deterministically; approvals from an
|
|
102
|
+
older `evidence_sha`, a non-`faithful` verdict, or an unverified attestation
|
|
103
|
+
never count. A required tier also fails closed when the current record has
|
|
104
|
+
no usable `surface_ref`/effect binding, so a caller cannot suppress a
|
|
105
|
+
configured rule by declaring a safe path.
|
|
106
|
+
|
|
107
|
+
`canon evidence add --surface-ref src/auth/login.rs` (repeatable) or
|
|
108
|
+
`--surface-ref effect:secret-access` persists an explicitly validated
|
|
109
|
+
binding. This is still attestation input, not proof of the changed paths;
|
|
110
|
+
run/diff metadata is authoritative when available, and an unavailable
|
|
111
|
+
binding is a gate violation rather than a clean result.
|
|
112
|
+
|
|
113
|
+
Approval identity is separate from attestation text. `--approval-by` is
|
|
114
|
+
accepted as a verified approval only when it equals a non-empty
|
|
115
|
+
`CANON_ACTOR` environment identity and `--approval-role human` is supplied.
|
|
116
|
+
The record persists that verification bit for replay; arbitrary caller
|
|
117
|
+
strings, empty identities, and `agent` roles never satisfy a risk tier.
|
|
118
|
+
Malformed `risk_tiers` sections produce a stable `uncovered-cell` policy
|
|
119
|
+
violation (`risk_tiers policy-invalid: ...`) instead of degrading to an
|
|
120
|
+
empty clean map.
|
|
121
|
+
|
|
122
|
+
## `canon gate task <task_id> [--repo <dir>]`
|
|
123
|
+
|
|
124
|
+
The evidence-gated task checkbox flip. Resolves `<task_id>`
|
|
125
|
+
(`<change_id>#<n>`) through `canon.yaml`'s `plans:` sources — the first
|
|
126
|
+
configured dialect whose `PlanWriteBack` locates the task's document
|
|
127
|
+
wins, so the flip is plan-dialect agnostic (`openspec` change dirs,
|
|
128
|
+
`superpowers` plan docs). A repo with no `plans:` section falls back to
|
|
129
|
+
the compat default `[{ dialect: openspec, root: <repo> }]`; no source
|
|
130
|
+
locating the task at all is a loud failure naming every source consulted.
|
|
131
|
+
It requires
|
|
132
|
+
a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
|
|
133
|
+
with an appended evidence note ONLY on a clean check. Every other path —
|
|
134
|
+
missing evidence, a `Divergent` verdict, a fabricated note — leaves the
|
|
135
|
+
row byte-unchanged and exits `1` with the blocking violation on stderr.
|
|
136
|
+
An already-`[x]` row is an idempotent no-op (exit `0`). An unknown
|
|
137
|
+
`task_id` is reported (exit `1`).
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
canon gate task my-change#5.2 --repo .
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This is canon's own authority for the checkbox grammar — always flip
|
|
144
|
+
through `canon gate task`, never by hand.
|
|
145
|
+
|
|
146
|
+
## `canon gate promote [--repo <dir>] [--dry-run]`
|
|
147
|
+
|
|
148
|
+
Staging → committed: every well-formed record under
|
|
149
|
+
`.canon/ledger/_staging/` is re-validated with the SAME checks the gate
|
|
150
|
+
applies, assigned a monotonic per-(role, surface) `run_seq` (gap-free
|
|
151
|
+
within one invocation, continuing from the committed max), and moved into
|
|
152
|
+
the committed ledger. A malformed or unpartitionable candidate is refused
|
|
153
|
+
— exit `1`, no `run_seq` consumed, the file left in place. `--dry-run`
|
|
154
|
+
prints the plan (target path + assigned `run_seq` per candidate) without
|
|
155
|
+
writing or deleting.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
canon gate promote --repo . # land every staged candidate
|
|
159
|
+
canon gate promote --repo . --dry-run # preview only
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
|
|
163
|
+
|
|
164
|
+
Idempotent, diff-only hook-seam installation: merges one
|
|
165
|
+
`{matcher?, hooks: [{type: "command", command, timeout}]}` entry into
|
|
166
|
+
BOTH `<repo>/.claude/settings.json` and `<repo>/.codex/hooks.json` —
|
|
167
|
+
additive only, never touching an existing entry in the same matcher
|
|
168
|
+
group. Running it twice with no manual edits between reports "no diff"
|
|
169
|
+
and writes nothing. When neither file already carries a `canon
|
|
170
|
+
gate`-invoking command, it ALSO emits
|
|
171
|
+
`<repo>/.canon/scripts/canon-gate-pre-commit.sh` (advisory by default — set
|
|
172
|
+
`CANON_GATE_ADVISORY=0` to make it block the commit on a failing gate).
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
canon gate install-hooks --repo .
|
|
176
|
+
# non-default wiring:
|
|
177
|
+
canon gate install-hooks --repo . --event PreToolUse --matcher Edit --command "canon gate check" --timeout 30
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Prefer this over hand-editing `settings.json`/`hooks.json`.
|
|
181
|
+
|
|
182
|
+
## `canon gate selftest`
|
|
183
|
+
|
|
184
|
+
Runs the shipped fixture corpus — one fixture per failure class, each a
|
|
185
|
+
deliberately broken corpus proving that class fires and ONLY that class
|
|
186
|
+
(both under-detection and over-triggering fail the run). Takes no
|
|
187
|
+
`--repo`; self-contained. Run it before trusting any other `canon gate
|
|
188
|
+
check` run's green.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
canon gate selftest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Reading a violation
|
|
195
|
+
|
|
196
|
+
Every printed line is `<failure-class> <subject> — <detail>`. `<subject>`
|
|
197
|
+
is the artifact's own join identity (`task_id` preferred, then
|
|
198
|
+
`scenario_id`, then `run_id`) — grep the ledger or `tasks.md` for it
|
|
199
|
+
directly, never guess.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# canon-inventory
|
|
2
|
+
|
|
3
|
+
`canon inventory sync` is the FIRST stage of canon's unified spec loop:
|
|
4
|
+
it turns a hand-authored, `canon format`-validated `.feature` corpus into
|
|
5
|
+
the scenario ledger-INDEX records every downstream stage joins against.
|
|
6
|
+
|
|
7
|
+
## The unified loop
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
author .feature corpus → canon format --check (validate)
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
canon inventory sync # THIS skill — materializes scenario index records
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
canon review add # attests a review — see canon-inventory downstream / canon-gate
|
|
17
|
+
canon divergence {stage,promote,resolve,defer,status} # tracks/resolves a divergence
|
|
18
|
+
canon gate check [--release] # trust-ladder / staleness / coverage — see canon-gate
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
canon ingest artifacts # with artifacts.native_records: true in canon.yaml —
|
|
22
|
+
# feeds review/divergence verdicts into the flywheel
|
|
23
|
+
# (see canon-artifact-ingest)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Every stage after `sync` joins on the SAME `(project_id, scenario_id)`
|
|
27
|
+
pair `sync` materializes — one identity scheme, no second one.
|
|
28
|
+
|
|
29
|
+
## `canon inventory sync [--repo <dir>] [--spec-root <dir>]`
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
canon inventory sync # every canon.yaml specs.roots[] entry
|
|
33
|
+
canon inventory sync --repo ../svc # a specific repo
|
|
34
|
+
canon inventory sync --spec-root ./specs # ad hoc: ignore canon.yaml entirely
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Per configured root, in order:
|
|
38
|
+
|
|
39
|
+
1. **Validate** — runs `canon format --check` over the root. ANY violation
|
|
40
|
+
(missing provenance, layout-grammar, …) ABORTS THE WHOLE ROOT: zero
|
|
41
|
+
records written for it, the violation(s) reported. Never a partial sync.
|
|
42
|
+
2. **Scan** — walks `<root>/features/**/*.feature`, pairing each
|
|
43
|
+
`@<area>.<surface>.<nn>` tag with its header label as `title` and a
|
|
44
|
+
`source_digest` (sha256 over the raw `.feature` bytes). `@subject:<id>`
|
|
45
|
+
→ `subject_id`, `@lane:<v>` → `lane` (first tag wins; a second, a
|
|
46
|
+
malformed value, or a value outside a declared `lane` enum is dropped
|
|
47
|
+
with a counted, non-fatal diagnostic). Any other `@name:value`
|
|
48
|
+
namespace is counted as a diagnostic so a dropped classification is
|
|
49
|
+
visible; plain tags (`@p2`) are ignored. The index derives from the
|
|
50
|
+
`.feature` corpus alone. See `canon-authoring` for the organizing rule.
|
|
51
|
+
3. **Materialize** — upserts ONE scenario index record per
|
|
52
|
+
`(project_id, scenario_id)` via the append-only git-tier write.
|
|
53
|
+
Logically idempotent: an unchanged `source_digest` is a no-op; a
|
|
54
|
+
changed `.feature` file appends exactly one new record. A `.feature`
|
|
55
|
+
edit re-materializes every scenario in that file (file granularity).
|
|
56
|
+
4. **Duplicate guard** — a `scenario_id` scanned more than once WITHIN
|
|
57
|
+
one root's corpus aborts that root (0 writes), reported as a sync
|
|
58
|
+
error. Two DIFFERENT roots sharing a `scenario_id` stay distinct —
|
|
59
|
+
`project_id` isolates them.
|
|
60
|
+
|
|
61
|
+
The scenario index shape is deliberately general: `title` +
|
|
62
|
+
`source_digest`, nothing else. It carries no coverage field — coverage
|
|
63
|
+
stays `canon gate check`'s own authority (see `canon-gate`).
|
|
64
|
+
|
|
65
|
+
## `specs.roots[]` config (`canon.yaml`)
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
specs:
|
|
69
|
+
roots:
|
|
70
|
+
- id: app-a # STABLE LITERAL — never the checkout directory name
|
|
71
|
+
root: apps/a/specs
|
|
72
|
+
- id: app-b
|
|
73
|
+
root: apps/b/specs
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Absent `specs:` key** → the single default root `{id: root, root: specs}`.
|
|
77
|
+
- **Present `specs:` with an empty/missing `roots[]`** → fails LOUD; a
|
|
78
|
+
present-but-incomplete config never silently resolves to zero roots.
|
|
79
|
+
- **A malformed entry** (missing `id`, `roots` not a list, `id` not a
|
|
80
|
+
valid project id) → fails LOUD, never a silent fallback to the default.
|
|
81
|
+
- `--spec-root <dir>` bypasses `specs.roots[]` entirely and syncs
|
|
82
|
+
exactly that one directory under the same stable `root` id the
|
|
83
|
+
absent-`specs:` default uses.
|
|
84
|
+
|
|
85
|
+
## Sync-abort causes
|
|
86
|
+
|
|
87
|
+
- **A `canon format --check` validation violation** in the root's corpus →
|
|
88
|
+
whole root aborts, 0 writes, violations reported.
|
|
89
|
+
- **A duplicate `scenario_id`** within one root → that root aborts, 0
|
|
90
|
+
writes, reported as a sync error (a distinct lane from fmt violations).
|
|
91
|
+
|
|
92
|
+
## How the index feeds the rest
|
|
93
|
+
|
|
94
|
+
- **Review** — `canon review add` attests a verdict against a synced
|
|
95
|
+
scenario. (See `canon-inventory` downstream commands and each command's `--help`.)
|
|
96
|
+
- **Divergence** — `canon divergence {stage,promote,resolve,defer,status}`
|
|
97
|
+
tracks/resolves divergences against synced scenarios.
|
|
98
|
+
- **Gate** — `canon gate check` enforces trust-ladder / staleness /
|
|
99
|
+
coverage over the corpus (see `canon-gate`).
|
|
100
|
+
- **Ingest** — `canon ingest artifacts` with `artifacts.native_records:
|
|
101
|
+
true` in `canon.yaml` feeds review/divergence verdicts into the
|
|
102
|
+
reward flywheel (see `canon-artifact-ingest`).
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# canon-learn
|
|
2
|
+
|
|
3
|
+
canon's role-namespaced strategy memory turns accumulated verdict
|
|
4
|
+
trajectories into retrievable, promotable insights. Two tiers:
|
|
5
|
+
|
|
6
|
+
- **Trajectories (warm, local).** Raw verdict-keyed rows under
|
|
7
|
+
`<repo>/.canon/learn/trajectories/`, written by ingest. Rebuildable,
|
|
8
|
+
gitignored, never hand-committed.
|
|
9
|
+
- **Strategies (durable, git).** Distilled, reviewable
|
|
10
|
+
`<repo>/.canon/strategies/<role>/<id>.md` files — the tier a promotion
|
|
11
|
+
writes into and a demotion soft-flags.
|
|
12
|
+
|
|
13
|
+
## Rebuilding strategy memory
|
|
14
|
+
|
|
15
|
+
Strategy memory is re-derived from the current trajectory set as part of
|
|
16
|
+
`canon ingest artifacts` (see `canon-artifact-ingest`): after it
|
|
17
|
+
persists a repo's verdict trajectories, it re-derives the distilled
|
|
18
|
+
strategies. This is NON-DESTRUCTIVE — a rebuild never drops a
|
|
19
|
+
hand-promoted strategy the trajectory rows don't touch. There is no
|
|
20
|
+
standalone `canon learn` rebuild command; the only subcommand is
|
|
21
|
+
`promote`.
|
|
22
|
+
|
|
23
|
+
# `canon learn promote <strategy_id> [--repo <dir>] [--evaluation <path> --approval <path>] [--signature <path>] [--dry-run]`
|
|
24
|
+
|
|
25
|
+
The production activation path is a signed, paired-evaluation workflow:
|
|
26
|
+
|
|
27
|
+
1. Run `canon ingest artifacts`. Ingest resolves covering verdicts and rebuilds
|
|
28
|
+
newly distilled strategies as `lifecycle: quarantined`.
|
|
29
|
+
2. Check eligibility with the configured `occurrence` or `crn` gate. This is
|
|
30
|
+
only a prerequisite; occurrence alone MUST NOT activate a strategy.
|
|
31
|
+
3. An honest external evaluator writes a `PromotionEvaluation` JSON bundle.
|
|
32
|
+
It MUST bind the candidate's id, version, quarantined lifecycle, strategy
|
|
33
|
+
digest, source trajectory ids, baseline and candidate result digests, and
|
|
34
|
+
non-empty corpus/evaluator/context/policy/model/tool provenance.
|
|
35
|
+
4. Require paired evidence: baseline and candidate digest arrays MUST be
|
|
36
|
+
non-empty, equal-length, disjoint, and duplicate-free. The evaluation's
|
|
37
|
+
finite `paired_metrics` MUST include `pairs`, `uplift`, and `regressions`;
|
|
38
|
+
`pairs` MUST equal the array length, `uplift` MUST be strictly positive,
|
|
39
|
+
`regressions` MUST be zero, `passed` MUST match those derived checks, and
|
|
40
|
+
`decision` MUST be `pass`, `promote`, or `approved`. Missing or unknown
|
|
41
|
+
metrics are rejected; the repository currently publishes **no measured
|
|
42
|
+
uplift result**, so this command is not evidence that retrieval improves
|
|
43
|
+
agent outcomes.
|
|
44
|
+
5. Produce the unsigned approval payload:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
canon learn approve <strategy_id> --evaluation evaluation.json \
|
|
48
|
+
--principal alice --repo .
|
|
49
|
+
# `request` is an exact alias:
|
|
50
|
+
canon learn request <strategy_id> --evaluation evaluation.json \
|
|
51
|
+
--principal alice --repo .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The JSON output contains the evaluation digest, RFC3339 `approved_at`,
|
|
55
|
+
subject `strategy:<strategy_id>`, namespace
|
|
56
|
+
`canon-learning-approval-v1`, and `payload_hex`. `approve`/`request` never
|
|
57
|
+
read a private key and never set `verified`.
|
|
58
|
+
6. Decode `payload_hex` to the exact canonical JSON bytes. The signed object
|
|
59
|
+
is:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"namespace": "canon-learning-approval-v1",
|
|
64
|
+
"subject": "strategy:<strategy_id>",
|
|
65
|
+
"project": null,
|
|
66
|
+
"artifact_sha": "<evaluation_digest>",
|
|
67
|
+
"run_id": null,
|
|
68
|
+
"surface": [],
|
|
69
|
+
"effects": [],
|
|
70
|
+
"actor": "<principal>",
|
|
71
|
+
"timestamp": "<RFC3339 nanosecond timestamp>"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For rollback, the corresponding signed object uses subject
|
|
76
|
+
`strategy:<id>:rollback:<sha256(reason)>`, `artifact_sha` equal to that
|
|
77
|
+
reason digest, and the rollback actor/timestamp. Sign the decoded bytes
|
|
78
|
+
externally with an SSH key using the `canon-learning-approval-v1`
|
|
79
|
+
namespace (for example, `ssh-keygen -Y sign -n canon-learning-approval-v1
|
|
80
|
+
-f <private-key>`). Put the armored detached signature in
|
|
81
|
+
`PromotionApproval.signature` and set `signer_key` to the same principal.
|
|
82
|
+
The approval JSON has exactly these fields: `schema_version`,
|
|
83
|
+
`candidate_strategy_id`, `evaluation_digest`, `approver_identity`,
|
|
84
|
+
`approver_role` (`human`), `verified` (non-authoritative display data),
|
|
85
|
+
`approved_at`, optional `signature`, optional `signer_key`, and
|
|
86
|
+
`integrity_digest`.
|
|
87
|
+
7. Configure the verifier in the repository policy:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
approval:
|
|
91
|
+
allowed_signers: .canon/allowed_signers
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The path is policy-pinned (relative paths resolve from the repository).
|
|
95
|
+
The verifier runs `ssh-keygen -Y verify` against that file; an absent,
|
|
96
|
+
unreadable, or non-matching signer fails closed. A detached signature is
|
|
97
|
+
authentication of the configured principal, not proof that the principal
|
|
98
|
+
is a human or is otherwise authorized outside this policy.
|
|
99
|
+
8. Activate only with both evidence files:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
canon learn promote <strategy_id> --repo . \
|
|
103
|
+
--evaluation evaluation.json --approval approval.json
|
|
104
|
+
# Or attach an armored signature file and recompute the approval digest:
|
|
105
|
+
canon learn promote <strategy_id> --repo . \
|
|
106
|
+
--evaluation evaluation.json --approval approval.json \
|
|
107
|
+
--signature approval.sig
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`--evaluation` and `--approval` MUST be supplied together. A quarantined
|
|
111
|
+
candidate without valid paired evaluation, policy-pinned SSH verification,
|
|
112
|
+
and human approval remains quarantined. `--dry-run` previews the render but
|
|
113
|
+
does not activate or write.
|
|
114
|
+
|
|
115
|
+
The command resolves the repo via the nearest-`canon.yaml`-ancestor walk,
|
|
116
|
+
evaluates the role gate before any write, and runs advisory lints
|
|
117
|
+
(content-length ceiling and literal-absolute-path rejection). There is no
|
|
118
|
+
`--force`. Promotion re-renders the whole git-tier file with YAML front matter
|
|
119
|
+
(`status`, `regime_key`, `role`, `title`, `source_trajectory_ids`,
|
|
120
|
+
`recorded_at`) plus evaluation and approval digests; a re-promote does not
|
|
121
|
+
preserve manual edits.
|
|
122
|
+
|
|
123
|
+
An unknown `strategy_id` fails nonzero. A candidate's lifecycle defaults to
|
|
124
|
+
`quarantined` when newly distilled and quarantine is excluded from retrieval.
|
|
125
|
+
The old public writer is retained only for already-`active` items; it is not a
|
|
126
|
+
way to activate new or legacy candidates.
|
|
127
|
+
|
|
128
|
+
### The eligibility gates: `crn` vs `occurrence`
|
|
129
|
+
|
|
130
|
+
A role's eligibility gate is a `canon.yaml` `learn:`-section choice:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
# canon.yaml
|
|
134
|
+
learn:
|
|
135
|
+
promotion:
|
|
136
|
+
dev:
|
|
137
|
+
mode: occurrence
|
|
138
|
+
n_min: 8 # optional — defaults to 5
|
|
139
|
+
window_days: 14 # optional — defaults to 30
|
|
140
|
+
sim:
|
|
141
|
+
mode: crn
|
|
142
|
+
demotion:
|
|
143
|
+
hard_delete: false # optional — soft-flag is the default
|
|
144
|
+
strategies_root: .canon/strategies # optional
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A role with no explicit `promotion.<role>` entry defaults to
|
|
148
|
+
`mode: occurrence, n_min: 5, window_days: 30`.
|
|
149
|
+
|
|
150
|
+
- **`occurrence`** — for roles whose domain does NOT support deterministic
|
|
151
|
+
replay. Within the trailing window, at least `n_min` resolved `Success`
|
|
152
|
+
trajectories for the same `regime_key` are required and no `Failure` or
|
|
153
|
+
`RolledBack` trajectory may be present. A contradiction resets the count;
|
|
154
|
+
it is never averaged away. This eligibility does not replace paired
|
|
155
|
+
evaluation or signed approval.
|
|
156
|
+
- **`crn`** — for roles that CAN run a deterministic simulator. A paired
|
|
157
|
+
common-random-number statistical eligibility gate uses the same
|
|
158
|
+
`crn:config=<label>` and `crn:panel=<index>` panels across compared
|
|
159
|
+
configurations. Insufficient panels/noise evidence is not a pass. CRN
|
|
160
|
+
eligibility still does not replace the external `PromotionEvaluation` and
|
|
161
|
+
signature.
|
|
162
|
+
|
|
163
|
+
Both gates read resolved `verdict_record.outcome`, not raw verdict rows.
|
|
164
|
+
`Pending` trajectories neither corroborate nor contradict. `canon ingest
|
|
165
|
+
artifacts` is therefore always first:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
canon ingest artifacts
|
|
169
|
+
canon learn promote <id> --evaluation evaluation.json --approval approval.json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Demotion versus authenticated rollback
|
|
173
|
+
|
|
174
|
+
A later ordinary contradictory `Failure`/`RolledBack` trajectory demotes an
|
|
175
|
+
already-active strategy when a non-dry-run promotion observes it. The default
|
|
176
|
+
is append-only soft-flagging of the existing git file with `status: demoted`
|
|
177
|
+
and `reason:`; `demotion.hard_delete: true` deletes that file instead. This
|
|
178
|
+
ordinary contradiction path is not an approval and does not create a rollback
|
|
179
|
+
record.
|
|
180
|
+
|
|
181
|
+
An operator-requested rollback is a distinct authenticated action:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
canon learn rollback <strategy_id> --reason "<reason>" --actor alice \
|
|
185
|
+
--signature-file rollback.sig --approved-at 2026-10-02T12:00:00Z \
|
|
186
|
+
[--contradicting-trajectory-id <trajectory_id>] --repo .
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`--signature-file` is an armored SSH detached signature over the
|
|
190
|
+
`canon-learning-approval-v1` payload bound to
|
|
191
|
+
`strategy:<id>:rollback:<sha256(reason)>`, actor, and `--approved-at`.
|
|
192
|
+
`actor` MUST be listed in policy-pinned `approval.allowed_signers`; verification
|
|
193
|
+
uses that file and `ssh-keygen -Y verify`. Successful rollback writes durable
|
|
194
|
+
provenance, sets lifecycle `rolled_back`, and records the optional contradicting
|
|
195
|
+
trajectory. Missing policy, signer, timestamp, or signature fails closed.
|
|
196
|
+
|
|
197
|
+
### What retrieval can and cannot serve
|
|
198
|
+
|
|
199
|
+
Retrieval serves only exact role/regime matches and excludes demoted,
|
|
200
|
+
quarantined, rejected, and rolled-back strategies. Legacy rows with
|
|
201
|
+
`lifecycle: null` remain readable for backward compatibility, but they have no
|
|
202
|
+
quarantine/evaluation/signature provenance and MUST NOT be treated as proof of
|
|
203
|
+
the current activation workflow. Retrieval is fail-soft and advisory; it does
|
|
204
|
+
not upgrade legacy rows or establish measured uplift.
|
|
205
|
+
|
|
206
|
+
## How verdicts feed scoring
|
|
207
|
+
|
|
208
|
+
Verdicts arrive from `canon ingest artifacts` (see
|
|
209
|
+
`canon-artifact-ingest`), persisted regime-keyed
|
|
210
|
+
(`<role>/<repo>/<area>/<hash>`). Each verdict scores its covering
|
|
211
|
+
trajectory into `Success` / `Failure` / `RolledBack` / `Pending`; the
|
|
212
|
+
eligibility gate reads a regime's accumulated resolved verdicts, while the
|
|
213
|
+
paired evaluation and signed approval decide whether a quarantined strategy
|
|
214
|
+
may become active. A contradicting trajectory for an already-promoted
|
|
215
|
+
strategy triggers ordinary demotion; an explicit rollback is separate and
|
|
216
|
+
authenticated.
|
|
217
|
+
|
|
218
|
+
## Reading a demoted or rolled-back strategy
|
|
219
|
+
|
|
220
|
+
An ordinary contradicting trajectory demotes an active strategy append-only:
|
|
221
|
+
the default soft-flag merges `status: demoted` + `reason: <text>` into the
|
|
222
|
+
existing front matter and leaves the body byte-unchanged. Set
|
|
223
|
+
`demotion.hard_delete: true` to delete the file instead. A strategy demoted
|
|
224
|
+
before it reached the git tier has no file to flag — that is not an error.
|
|
225
|
+
`canon retrieve` skips demoted and rolled-back strategies. An authenticated
|
|
226
|
+
rollback is different: it records signed operator provenance and sets the
|
|
227
|
+
stored lifecycle to `rolled_back`.
|
|
228
|
+
|
|
229
|
+
## The flywheel
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
ingest → resolved trajectories → quarantined strategies
|
|
233
|
+
↑ │
|
|
234
|
+
verdicts eligibility + paired evaluation + SSH approval
|
|
235
|
+
│ (activate)
|
|
236
|
+
▼
|
|
237
|
+
active git strategies → advisory retrieve
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## What this skill does NOT cover
|
|
241
|
+
|
|
242
|
+
- Retrieval at dispatch time (`canon retrieve`, the pre-dispatch hook) —
|
|
243
|
+
see the `canon-retrieve` skill.
|
|
244
|
+
- Producing the verdict trajectories promotion reads — see the
|
|
245
|
+
`canon-artifact-ingest` skill.
|