@journeykit/canon 0.7.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 +7 -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 -5
|
@@ -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.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# canon-plan-import
|
|
2
|
+
|
|
3
|
+
`canon ingest plans` imports a FOREIGN plan dialect's on-disk state into
|
|
4
|
+
canon's `change`/`task` records, through the same validated path every
|
|
5
|
+
other ingest family uses. Imported rows carry no special marker — once
|
|
6
|
+
imported, `canon query` cannot tell an imported row from a hand-authored
|
|
7
|
+
one, and neither can `canon gate`.
|
|
8
|
+
|
|
9
|
+
## 1. Configure a `plans:` source
|
|
10
|
+
|
|
11
|
+
`canon.yaml`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plans:
|
|
15
|
+
sources:
|
|
16
|
+
- dialect: openspec
|
|
17
|
+
root: . # resolved relative to canon.yaml's directory
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- An absent `plans:` section = zero sources (a clean no-op).
|
|
21
|
+
- A present section parses STRICTLY: a typo'd key, an unregistered
|
|
22
|
+
`dialect`, or a nonexistent `root` fails the command loud, naming the
|
|
23
|
+
offender.
|
|
24
|
+
- `root` is whatever the dialect's adapter scans — for `openspec`, a
|
|
25
|
+
repo root containing `openspec/changes/`, or that changes dir directly.
|
|
26
|
+
|
|
27
|
+
One-shot override (bypasses `canon.yaml` for a single ad hoc import;
|
|
28
|
+
either flag alone fails loud):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
canon ingest plans --dialect openspec --source ../other-repo
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 2. Run the import
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
canon ingest plans # canon.yaml-driven, every configured source
|
|
38
|
+
canon ingest plans --json # machine-readable outcome
|
|
39
|
+
canon ingest plans --repo ../svc # a specific repo root
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Per source the summary reports `changes_parsed`/`tasks_parsed`,
|
|
43
|
+
`changes_persisted`/`tasks_persisted`, `changes_unwritten`/
|
|
44
|
+
`tasks_unwritten` (an unreachable tier — printed, never silently
|
|
45
|
+
dropped), `duplicate_change_id` (two sources producing the same
|
|
46
|
+
`change_id` this pass — the LATER is skipped and counted, never merged),
|
|
47
|
+
the adapter's `unmapped` drop counts, and `malformed` (structurally
|
|
48
|
+
broken constructs). A source whose content is byte-identical to its last
|
|
49
|
+
successful run is `skipped_unchanged` — re-importing writes zero records.
|
|
50
|
+
|
|
51
|
+
## 3. Read the imported rows
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
canon query --kind change [--json]
|
|
55
|
+
canon query --kind task [--json]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
No dialect/plugin filter exists or is needed — an imported `change`/
|
|
59
|
+
`task` is an ordinary core record.
|
|
60
|
+
|
|
61
|
+
## The `openspec` dialect
|
|
62
|
+
|
|
63
|
+
Maps `openspec/changes/**` onto core kinds:
|
|
64
|
+
|
|
65
|
+
| openspec construct | Core mapping |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| change dir's basename | `change_id` (verbatim — no slug massaging) |
|
|
68
|
+
| `proposal.md`'s `## Why` first paragraph | `Change.summary` (absent heading → empty summary + a `proposal-missing-why` diagnostic) |
|
|
69
|
+
| archive location vs. checkbox tallies | `Change.status` (`archived` wins; else zero rows → `proposed`, all done → `completed`, mixed → `in_progress`) |
|
|
70
|
+
| each `tasks.md` checkbox row | `Task` (`task_id = <change_id>#<n>`, status verbatim, evidence_note from a ` — ✅ ` suffix or a `**DEFERRED**`/`**DROPPED**` annotation) |
|
|
71
|
+
| `specs/**/spec.md` `#### Scenario:` blocks | dropped, counted under `spec-delta-scenario` |
|
|
72
|
+
| `design.md` | dropped, counted under `design-doc` |
|
|
73
|
+
|
|
74
|
+
A missing/unreadable `proposal.md`, or a basename that isn't a valid
|
|
75
|
+
`change_id`, skips the WHOLE dir (counted `malformed`); siblings
|
|
76
|
+
unaffected. A proposal-only dir imports as `Change { status: proposed }`
|
|
77
|
+
with zero tasks and zero diagnostics.
|
|
78
|
+
|
|
79
|
+
## The `superpowers` dialect
|
|
80
|
+
|
|
81
|
+
Maps one plan document (an `*.md` immediate child of the plans root),
|
|
82
|
+
following the superpowers `writing-plans` skill's grammar:
|
|
83
|
+
|
|
84
|
+
| `writing-plans` construct | Core mapping |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| the filename stem, slugified (lowercased, each `[^a-z0-9]+` run → one `-`, edge `-` trimmed; date prefix kept verbatim) | `change_id` |
|
|
87
|
+
| the `**Goal:**` line's remainder, whitespace-normalized | `Change.summary` (absent → empty summary + a `goal-missing` diagnostic) |
|
|
88
|
+
| checkbox tallies across every `### Task N:` section | `Change.status` (zero tasks → `proposed`, all done → `completed`, mixed → `in_progress`) |
|
|
89
|
+
| each `### Task N: <name>` section | `Task` (`task_id = <change_id>#<N>`, title = text after the colon; status `done` iff the section has ≥1 checkbox line and ALL are checked, else `open`) |
|
|
90
|
+
| steps, `**Architecture:**`/`**Tech Stack:**` prose, Global Constraints, non-task headings | never read, never a diagnostic |
|
|
91
|
+
|
|
92
|
+
Worked example — `2026-07-14-website-design.md`:
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
# Website Implementation Plan
|
|
96
|
+
|
|
97
|
+
**Goal:** Build the project website.
|
|
98
|
+
|
|
99
|
+
### Task 1: Layout
|
|
100
|
+
|
|
101
|
+
- [x] **Step 1: scaffold the grid**
|
|
102
|
+
- [x] **Step 2: wire the nav**
|
|
103
|
+
|
|
104
|
+
### Task 2: Copy
|
|
105
|
+
|
|
106
|
+
- [x] **Step 1: draft the homepage**
|
|
107
|
+
- [ ] **Step 2: proofread**
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
imports as `Change { change_id: "2026-07-14-website-design", summary:
|
|
111
|
+
"Build the project website.", status: in_progress }`, plus
|
|
112
|
+
`Task { task_id: "…#1", title: "Layout", status: done }` and
|
|
113
|
+
`Task { task_id: "…#2", title: "Copy", status: open }`. The `**Step N:**`
|
|
114
|
+
bolding is NOT load-bearing — a plain `- [ ]`/`- [x]` row still counts.
|
|
115
|
+
|
|
116
|
+
An invalid task-number heading is skipped and named `malformed`; a
|
|
117
|
+
duplicate `Task N` heading keeps the first section and names the later one
|
|
118
|
+
`malformed`. A filename stem that fails to slug into a valid `change_id`
|
|
119
|
+
is malformed too. A markdown file with neither a `**Goal:**` line nor any
|
|
120
|
+
`### Task N:` heading (a stray `README.md`) is skipped with a
|
|
121
|
+
`not-a-plan-doc` diagnostic, not imported as garbage. Only immediate-child
|
|
122
|
+
`*.md` files are read, byte-lexically sorted; an absent/unreadable root
|
|
123
|
+
yields zero records, never an error.
|
|
124
|
+
|
|
125
|
+
## The task_id join
|
|
126
|
+
|
|
127
|
+
An imported `Task.task_id` is byte-identical to the join key of the
|
|
128
|
+
openspec-task verdict events canon's artifact ingest derives from the same
|
|
129
|
+
checkbox tree — so `canon report`'s trust matrix joins the two sides on
|
|
130
|
+
`task_id` for its coverage columns, and `canon ingest plans` is that
|
|
131
|
+
join's bulk producer. `canon ingest plans` itself performs no join and
|
|
132
|
+
writes no verdict/evidence — joining plan state against evidence is a
|
|
133
|
+
downstream reader's job.
|
|
134
|
+
|
|
135
|
+
## What this skill does NOT cover
|
|
136
|
+
|
|
137
|
+
- **Authoring an openspec change dir** (`proposal.md`/`tasks.md`
|
|
138
|
+
conventions) — see the openspec CLI's own docs; this skill covers
|
|
139
|
+
IMPORTING that shape, not producing it.
|
|
140
|
+
- **`canon gate check`/`canon gate task`** (the hand-authored one-task-at-
|
|
141
|
+
a-time path this complements at bulk) — see `canon-gate`.
|
|
142
|
+
- **Wiring an imported `Task`/`Change` into a `canon gate` decision** —
|
|
143
|
+
out of scope; needs its own reviewed change.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# canon-plugins
|
|
2
|
+
|
|
3
|
+
A plugin adds fields to a core record WITHOUT changing that record's
|
|
4
|
+
schema. It declares a NAMESPACED overlay kind (`<namespace>.<kind>`, e.g.
|
|
5
|
+
`porting.coverage`), writes overlay records into that namespace, and a
|
|
6
|
+
read-time JOIN projects the declared fields onto the core view. Core
|
|
7
|
+
records are NEVER rewritten by any step — the overlay write path is
|
|
8
|
+
append-only under a foreign namespace, and a manifest cannot alias a
|
|
9
|
+
core kind's on-disk path even by accident. Today overlays attach to the
|
|
10
|
+
`scenario` core kind only.
|
|
11
|
+
|
|
12
|
+
## 1. Author a `plugin.yaml` manifest
|
|
13
|
+
|
|
14
|
+
`.canon/plugins/<id>/plugin.yaml`:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
id: porting
|
|
18
|
+
namespace: porting
|
|
19
|
+
overlays:
|
|
20
|
+
- kind: coverage
|
|
21
|
+
attaches_to:
|
|
22
|
+
core_kind: scenario # `scenario` ONLY
|
|
23
|
+
join_key: [project_id, scenario_id]
|
|
24
|
+
fields:
|
|
25
|
+
- name: covered
|
|
26
|
+
type: bool
|
|
27
|
+
- name: surface_ref
|
|
28
|
+
type: { list: string }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `id`/`namespace`/`overlays[].kind` are REQUIRED — a missing field fails
|
|
32
|
+
the load outright, never defaulted.
|
|
33
|
+
- `namespace` and every overlay `kind` MUST match the kebab-token grammar
|
|
34
|
+
`[a-z0-9]+(-[a-z0-9]+)*` — else rejected with `E-PLUGIN-GRAMMAR`.
|
|
35
|
+
- `<namespace>.<kind>` MUST NOT collide with a core record kind name
|
|
36
|
+
(`E-PLUGIN-CORE-COLLISION`) and MUST be unique across installed plugins
|
|
37
|
+
(`E-PLUGIN-DUP-OVERLAY`).
|
|
38
|
+
- `attaches_to.core_kind` MUST be `scenario` — any other value fails with
|
|
39
|
+
`E-PLUGIN-CORE-KIND`.
|
|
40
|
+
- `type` is a bare scalar (`bool`/`number`/`string`), `{ enum: […] }`, or
|
|
41
|
+
`{ list: <type> }`.
|
|
42
|
+
- Two packages declaring the SAME `id`: the later (directory-sort order)
|
|
43
|
+
is dropped and diagnosed, never merged (`E-PLUGIN-DUP-ID`).
|
|
44
|
+
- An absent `.canon/plugins/` directory resolves an empty, valid snapshot —
|
|
45
|
+
never a panic.
|
|
46
|
+
|
|
47
|
+
## 2. Validate + write overlay records
|
|
48
|
+
|
|
49
|
+
Drive it through the sync command (never hand-write records):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
canon plugin sync porting # every canon.yaml specs.roots[] entry
|
|
53
|
+
canon plugin sync porting --repo ../svc # a specific repo
|
|
54
|
+
canon plugin sync porting --spec-root ./specs # ad hoc: ignore canon.yaml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`canon plugin sync <plugin-id>` resolves the manifest, then for each
|
|
58
|
+
candidate validates FIRST (envelope present + typed; every required
|
|
59
|
+
`join_key` field present + a string; every declared field present + type-
|
|
60
|
+
matching; no field outside envelope ∪ join-key ∪ declared), then writes.
|
|
61
|
+
The on-disk path is derived from the join-key field values in declared
|
|
62
|
+
order plus a body digest — a byte-identical resubmission dedupes to the
|
|
63
|
+
same path; a different body for the same join key appends at a new path,
|
|
64
|
+
never overwriting.
|
|
65
|
+
|
|
66
|
+
## 3. Project a read
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
canon query --kind scenario --plugin porting # human table, projected fields appended
|
|
70
|
+
canon query --kind scenario --plugin porting --json # merged record bodies
|
|
71
|
+
canon query --kind scenario # no --plugin: byte-identical to the core view
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--plugin <id>` folds the plugin's overlay records (latest-by-
|
|
75
|
+
`(join_key, at)`) onto each queried core record. Fail-soft everywhere:
|
|
76
|
+
|
|
77
|
+
- No overlay record for a core key → that record projects UNMODIFIED (no
|
|
78
|
+
value invented).
|
|
79
|
+
- A malformed overlay record (fails validation against the CURRENT
|
|
80
|
+
manifest — e.g. the schema drifted after the record was written) →
|
|
81
|
+
SKIPPED + diagnosed; siblings still project.
|
|
82
|
+
- An absent plugin/manifest → the unmodified core view + a diagnostic,
|
|
83
|
+
never a process error.
|
|
84
|
+
- Omitting `--plugin` is byte-identical to `canon query`'s output without
|
|
85
|
+
plugins — it's a purely additive layer.
|
|
86
|
+
|
|
87
|
+
## The `porting` worked example
|
|
88
|
+
|
|
89
|
+
`porting` moves the `covered`/`surface_ref` fields off the core
|
|
90
|
+
`scenario` record and into a plugin overlay:
|
|
91
|
+
|
|
92
|
+
1. `.canon/plugins/porting/plugin.yaml` — the manifest above.
|
|
93
|
+
2. Its overlay source reads a spec root's `inventory/**/*.yaml` (the same
|
|
94
|
+
inventory files `canon format --check` covers) and, for every
|
|
95
|
+
`(project_id, scenario_id)` `canon inventory sync` would index, emits
|
|
96
|
+
one candidate: `covered = true` iff that `scenario_id` appears in ANY
|
|
97
|
+
inventory entry's `covered_by` list; `surface_ref` = every inventory-
|
|
98
|
+
entry key whose `covered_by` contains it (empty when uncovered). The
|
|
99
|
+
candidate's envelope `at` is the source version (the max inventory-file
|
|
100
|
+
`at`), so an unchanged inventory re-syncs to a byte-identical body
|
|
101
|
+
(zero new files) and a genuine coverage edit supersedes the prior
|
|
102
|
+
overlay via latest-`at`-wins.
|
|
103
|
+
3. `canon plugin sync porting` writes/dedupes those candidates.
|
|
104
|
+
4. `canon query --kind scenario --plugin porting [--json]` projects
|
|
105
|
+
`covered`/`surface_ref` back onto each scenario.
|
|
106
|
+
|
|
107
|
+
`canon gate check`'s verdicts are byte-identical with and without a
|
|
108
|
+
`canon plugin sync porting` run — the gate reads NO plugin overlay. Wiring
|
|
109
|
+
an overlay field into a gate decision needs its own reviewed change
|
|
110
|
+
("covered ≠ coverage").
|
|
111
|
+
|
|
112
|
+
## Do not confuse the two `plugin.yaml` surfaces
|
|
113
|
+
|
|
114
|
+
The typed-authoring vocabulary (`.canon/vocab/<id>/plugin.yaml`: directives
|
|
115
|
+
+ enums for task-atom authoring — see `canon-vocab`) is a
|
|
116
|
+
DIFFERENT directory and a DIFFERENT schema. A ledger-overlay manifest
|
|
117
|
+
misplaced under `.canon/vocab/<id>/` fails that loader (missing
|
|
118
|
+
`directives`/`enums`), not silently parsing as an authoring vocabulary.
|
|
119
|
+
|
|
120
|
+
## What this skill does NOT cover
|
|
121
|
+
|
|
122
|
+
- **`canon inventory sync`** (the core index `porting`'s source reads) —
|
|
123
|
+
see `canon-inventory`.
|
|
124
|
+
- **`canon gate check`** — see `canon-gate`; the gate never reads a
|
|
125
|
+
plugin overlay.
|
|
126
|
+
- **`canon scenario new`/`canon feature new`** (corpus-authoring
|
|
127
|
+
scaffold, independent of the plugin machinery) — template generators
|
|
128
|
+
that write no ledger record.
|
|
129
|
+
- **A projection over a core kind other than `scenario`, or wiring an
|
|
130
|
+
overlay field into a `canon gate` decision** — out of scope; each needs
|
|
131
|
+
its own reviewed change.
|