@journeykit/canon 0.7.0 → 0.9.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 +345 -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 +172 -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,214 @@
|
|
|
1
|
+
# canon-session-ingest
|
|
2
|
+
|
|
3
|
+
`canon ingest sessions` turns every agent CLI's on-disk transcript store
|
|
4
|
+
into `Session`/`Run`/`Event` records on canon's join spine, scanning four
|
|
5
|
+
adapters, normalizing, and persisting through the same tier resolution
|
|
6
|
+
`canon query` / `canon tier age` use. The default corpus is scoped to the
|
|
7
|
+
current PROJECT; the watermark gate is per-file; user directives are
|
|
8
|
+
metadata-only unless explicitly opted in by bounded privacy config.
|
|
9
|
+
|
|
10
|
+
**Quick start needs zero docker/services.** `canon init` scaffolds `hot`
|
|
11
|
+
as a local sqlite file (`.canon/hot.db`) with `session`/`run`/`event`
|
|
12
|
+
already routed to it — a fresh repo's FIRST `canon ingest sessions`
|
|
13
|
+
persists real records with no `docker compose` stack, no `CANON_PG_DSN`,
|
|
14
|
+
no live database. Swap `hot` to postgres (the commented stanza `canon
|
|
15
|
+
init` ships beside the sqlite one) once team-scale multi-agent
|
|
16
|
+
concurrency outgrows sqlite's single-writer mode.
|
|
17
|
+
|
|
18
|
+
## `canon ingest sessions [--watch] [--interval-secs N] [--home <dir>] [--canon-yaml <path>] [--full] [--all-workspaces]`
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
canon ingest sessions # one pass over this project's sources
|
|
22
|
+
canon ingest sessions --watch # poll every 30s (see --interval-secs)
|
|
23
|
+
canon ingest sessions --full # ignore the watermark; re-parse every in-scope file
|
|
24
|
+
canon ingest sessions --all-workspaces # machine-wide scan
|
|
25
|
+
canon ingest sessions --home /custom # override the scan-root home ($HOME)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The four adapters
|
|
29
|
+
|
|
30
|
+
Registered in order: **`omp`** (omp/pi sessions), **`hermes`**,
|
|
31
|
+
**`claude`** (`~/.claude/projects/**/*.jsonl`), **`codex`**
|
|
32
|
+
(`${CODEX_HOME:-~/.codex}/sessions/` unioned with `archived_sessions/`).
|
|
33
|
+
Each resolves its default root and honors an env override
|
|
34
|
+
(`CANON_INGEST_OMP_SESSIONS_DIR`, `CANON_INGEST_CLAUDE_SESSIONS_DIR`,
|
|
35
|
+
`CODEX_HOME`, `HERMES_HOME`). An absent source root is a zero-record,
|
|
36
|
+
non-fatal skip.
|
|
37
|
+
|
|
38
|
+
### Per-source root overrides (`canon.yaml`)
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
ingest:
|
|
42
|
+
sources:
|
|
43
|
+
omp:
|
|
44
|
+
roots: [/data/omp-sessions] # relative paths resolve against the canon.yaml dir
|
|
45
|
+
codex:
|
|
46
|
+
roots: [] # explicit empty = scan ZERO (not the default)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A source with a **present** `roots` scans exactly those (explicit
|
|
50
|
+
`roots: []` scans nothing); a source whose key or `roots` is **absent**
|
|
51
|
+
keeps its env-override + default resolution. Unlike artifacts (where
|
|
52
|
+
unconfigured = no scan), sessions default to their home roots when
|
|
53
|
+
unconfigured. A PRESENT but broken `ingest:` section fails **loud** (a
|
|
54
|
+
typo like `root:` for `roots:`, an unknown source id, or a non-YAML
|
|
55
|
+
canon.yaml) rather than silently scanning the wrong corpus; a
|
|
56
|
+
missing/unreadable canon.yaml stays a soft no-config.
|
|
57
|
+
|
|
58
|
+
## Project scope — the default corpus
|
|
59
|
+
|
|
60
|
+
`canon ingest sessions` defaults to THIS PROJECT's sessions, never the
|
|
61
|
+
whole machine: the repo's main `git worktree` root plus every linked
|
|
62
|
+
worktree (`git worktree list --porcelain`). Outside a git repo, the scope
|
|
63
|
+
fails soft to the repo root alone.
|
|
64
|
+
|
|
65
|
+
- **omp/pi and Claude Code are cwd-partitioned on disk** — one
|
|
66
|
+
subdirectory per project. Out-of-scope subdirectories are pruned at
|
|
67
|
+
enumerate time — never read or counted.
|
|
68
|
+
- **Codex and Hermes are not partitioned** — rows are filtered
|
|
69
|
+
post-parse by workspace membership; a row with no captured workspace is
|
|
70
|
+
kept, fail-soft.
|
|
71
|
+
- **`--all-workspaces`** restores the machine-wide scan. Scope is still
|
|
72
|
+
resolved (so `project_key` stamping stays correct); only the
|
|
73
|
+
pruning/filtering are disabled.
|
|
74
|
+
- Every ingested session whose workspace resolves into the active project
|
|
75
|
+
gets `project_key` stamped to the MAIN worktree's key, so `canon query
|
|
76
|
+
--kind session` aggregates a whole project's sessions regardless of
|
|
77
|
+
which worktree recorded them.
|
|
78
|
+
- The run summary's first line always names the active scope:
|
|
79
|
+
`scope: project /repo/root (2 roots)` or `scope: all workspaces`.
|
|
80
|
+
|
|
81
|
+
### Provenance, digests, and parser generations
|
|
82
|
+
|
|
83
|
+
The parser semantics are compared against Tokscale upstream at
|
|
84
|
+
https://github.com/junhoyeo/tokscale (revision
|
|
85
|
+
`fe72e1f9b5a2b1a927108b353a9730a5c631a196`); the cache-accounting comparison
|
|
86
|
+
reference is commit
|
|
87
|
+
[`2cd6c648df026fb43d37e492efa9c344e2d681ee`](https://github.com/junhoyeo/tokscale/commit/2cd6c648df026fb43d37e492efa9c344e2d681ee).
|
|
88
|
+
Canon ports the selected parser semantics, including Claude's nested
|
|
89
|
+
cache-write buckets, but does **not** vendor Tokscale's pricing or cache
|
|
90
|
+
service.
|
|
91
|
+
|
|
92
|
+
Three identities are intentionally distinct:
|
|
93
|
+
|
|
94
|
+
* the raw file SHA-256 watermark identifies unchanged transcript bytes;
|
|
95
|
+
* the normalized content digest is canonical JSON over the complete
|
|
96
|
+
normalized event and changes when normalized token detail changes; and
|
|
97
|
+
* the parse-generation cursor identity combines adapter id and parse version,
|
|
98
|
+
forcing a re-read when parser semantics change even if raw bytes do not.
|
|
99
|
+
|
|
100
|
+
## The watermark — per-file, incremental `--watch`
|
|
101
|
+
|
|
102
|
+
Each pass content-digests every present (post-pruning) file and diffs it
|
|
103
|
+
against its source's persisted cursor (under
|
|
104
|
+
`<repo>/.canon/ingest/cursors/`, gitignored), written after a durable
|
|
105
|
+
pass. A file whose digest matches its cursor is SKIPPED — never parsed; a
|
|
106
|
+
new or changed file is (re-)parsed. One session derives from exactly one
|
|
107
|
+
file, so a single growing transcript re-parses alone instead of dragging
|
|
108
|
+
the whole source back through parse/persist.
|
|
109
|
+
|
|
110
|
+
- The gate is sound: a file is skipped only when its digest matches the
|
|
111
|
+
cursor, so a new, changed, copied, or restored file is always
|
|
112
|
+
re-scanned.
|
|
113
|
+
- Correctness never depends on the cursor: a missing/corrupt cursor
|
|
114
|
+
treats every present file as new, and the digest-idempotent write path
|
|
115
|
+
keeps any rescan from double-writing.
|
|
116
|
+
- A cursor's identity carries its adapter's PARSE VERSION as well as its
|
|
117
|
+
files, so changing an adapter's normalization re-reads that adapter's
|
|
118
|
+
transcripts instead of reporting them unchanged — no `--full`, no
|
|
119
|
+
cursor deletion. Version `1` IS the bare `client_id` (`omp.json`), so a
|
|
120
|
+
cursor already on disk is never invalidated just by installing the
|
|
121
|
+
mechanism; a bump appends the suffix (`claude-code-v3.json`) and that
|
|
122
|
+
`omp` is at `2` because the logical adapter now deduplicates forks
|
|
123
|
+
across files; `hermes` remains at `1`; `codex` is at `2` because its
|
|
124
|
+
reasoning-token accounting is split; and `claude-code` is at `3` because
|
|
125
|
+
cache accounting now recognizes `cache_write_1h` buckets. Each changed
|
|
126
|
+
adapter re-reads once to backfill the normalization change that the old
|
|
127
|
+
watermark would otherwise hide.
|
|
128
|
+
|
|
129
|
+
**`--full`** ignores the cursors and re-parses every present in-scope
|
|
130
|
+
file (a full rescan / cursor reset) — safe because a byte-identical
|
|
131
|
+
resubmission is a no-op; cursors re-advance afterward.
|
|
132
|
+
|
|
133
|
+
## User directives
|
|
134
|
+
|
|
135
|
+
Every adapter (except Hermes, whose format carries no user-turn text)
|
|
136
|
+
emits one parsed `DirectiveRow` per USER-role message. Parsing retains
|
|
137
|
+
source text only in memory; the shared post-adapter privacy boundary
|
|
138
|
+
controls whether it becomes a durable event. The default when
|
|
139
|
+
`ingest.sessions.privacy` is absent is metadata-only: no
|
|
140
|
+
`user_directive` event reaches normalized output.
|
|
141
|
+
|
|
142
|
+
Repositories that explicitly opt in MUST set a positive bound:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
ingest:
|
|
146
|
+
sessions:
|
|
147
|
+
privacy:
|
|
148
|
+
capture_user_directives: true
|
|
149
|
+
max_directive_chars: 4096
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Captured text is classified and redacted BEFORE the Unicode-scalar capture
|
|
153
|
+
bound is applied. The conservative classifier replaces known API
|
|
154
|
+
key/token/password assignments, bearer credentials, PEM private keys, and
|
|
155
|
+
email addresses with deterministic `[REDACTED_SECRET_N]` /
|
|
156
|
+
`[REDACTED_PII_N]` placeholders and includes `redaction_counts`. The
|
|
157
|
+
detail's `redacted` is true only when a known value was replaced;
|
|
158
|
+
`truncated` is independent and may be true for otherwise unredacted text.
|
|
159
|
+
This classifier is not a guarantee against every kind of secret or PII.
|
|
160
|
+
`workspace_key`/`workspace_label` remain metadata. The source transcript
|
|
161
|
+
remains the source of truth and is not rewritten, purged, or replaced by
|
|
162
|
+
Canon's normalized event.
|
|
163
|
+
|
|
164
|
+
The source cursor identity includes the privacy mode and positive bound, so
|
|
165
|
+
toggling capture or changing its bound re-scans unchanged source bytes.
|
|
166
|
+
Re-scans retain stable event sequence/key ordinals; they do not purge
|
|
167
|
+
previously stored normalized records. Operators owning pre-existing raw
|
|
168
|
+
data MUST handle purge/migration separately.
|
|
169
|
+
|
|
170
|
+
These events interleave with `token_usage` events in one deterministic
|
|
171
|
+
order (directive-before-token on a timestamp tie). Query them with:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
canon query --kind event # filter detail.label == "user_directive"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Reading the run summary
|
|
178
|
+
|
|
179
|
+
Each pass prints the active scope, one line per adapter, then totals:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
scope: project /Users/me/Workspace/canon (2 roots)
|
|
183
|
+
omp: 3 file(s) scanned, 1 reparsed, 2 skipped unchanged (watermark), 4 row(s) parsed, 0 malformed record(s)
|
|
184
|
+
claude-code: 1 file(s) scanned, 0 reparsed, 1 skipped unchanged (watermark), 0 row(s) parsed, 0 malformed record(s)
|
|
185
|
+
sessions normalized: 4
|
|
186
|
+
malformed records (corrupt line/db, counted as violations): 0
|
|
187
|
+
rows skipped (malformed session_id): 0
|
|
188
|
+
runs written: 4
|
|
189
|
+
events written: 37
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- **scope** — the active project scope (`--all-workspaces` names it).
|
|
193
|
+
- **file(s) scanned** — every present file this source matched, after
|
|
194
|
+
pruning, before the per-file gate.
|
|
195
|
+
- **reparsed** — the subset actually (re-)parsed this pass (`0` on a
|
|
196
|
+
steady-state pass).
|
|
197
|
+
- **skipped unchanged (watermark)** — files skipped because that file's
|
|
198
|
+
digest was byte-identical to its cursor entry.
|
|
199
|
+
- **malformed records** — corrupt lines / dbs an adapter hit but could
|
|
200
|
+
not extract a row from (counted as a violation, never crashing).
|
|
201
|
+
- **store tiers unreachable** — if `canon.yaml`'s tiers aren't reachable
|
|
202
|
+
(e.g. `tiers.pg` set but `CANON_PG_DSN` unset) or `session`/`run`/
|
|
203
|
+
`event` aren't routed, the pass prints a metadata-only JSON fallback
|
|
204
|
+
instead of persisting — never a partial write. The fallback contains
|
|
205
|
+
scope, adapter counts, record counts, IDs/digests, and a stable
|
|
206
|
+
routing/tier failure class/reason; it never contains directive text,
|
|
207
|
+
task/context prose, or raw event detail.
|
|
208
|
+
|
|
209
|
+
## What this skill does NOT cover
|
|
210
|
+
|
|
211
|
+
- Artifact/verdict ingestion (`canon ingest artifacts`) — a different
|
|
212
|
+
pipeline; see the `canon-artifact-ingest` skill.
|
|
213
|
+
- Cost-parity computation — omp/pi's own cost is `0.0`/`Unknown` per the
|
|
214
|
+
ported behavior.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# canon-storage
|
|
2
|
+
|
|
3
|
+
canon stores every record in one of three capability **rungs**:
|
|
4
|
+
|
|
5
|
+
- `local` — diffable files in the repo (git-backed), zero network.
|
|
6
|
+
- `hot` — live-queryable state (sqlite or postgres).
|
|
7
|
+
- `cold` — bulk archive (s3).
|
|
8
|
+
|
|
9
|
+
A rung is a *role*; which backend implements it is a separate
|
|
10
|
+
declaration. `routing:`/`aging:` name a rung (`local`/`hot`/`cold`),
|
|
11
|
+
never a backend name. `tiers.<rung>.backend:` names the backend.
|
|
12
|
+
|
|
13
|
+
## Backends and their config
|
|
14
|
+
|
|
15
|
+
| backend | rung role | `tiers.<rung>` config | credentials / network |
|
|
16
|
+
| ---------- | --------- | ---------------------------------- | --------------------- |
|
|
17
|
+
| `git` | local | `root:` (relative to canon.yaml) | none — zero network |
|
|
18
|
+
| `sqlite` | hot | `path:` (relative to canon.yaml) | none |
|
|
19
|
+
| `postgres` | hot | `dsn_env:` + `schema:` | live network + creds |
|
|
20
|
+
| `s3` | cold | `bucket_env:` + `prefix:` | live network + creds |
|
|
21
|
+
|
|
22
|
+
`canon init` scaffolds `sqlite` as the zero-dependency `hot` default
|
|
23
|
+
(WAL journal + busy timeout, fine for one operator's batch ingest).
|
|
24
|
+
sqlite is a **single-writer store**: for heavy multi-agent concurrent
|
|
25
|
+
writers, swap the `hot` rung to `postgres` — same hot role, a one-block
|
|
26
|
+
canon.yaml swap (the scaffold ships the postgres stanza commented right
|
|
27
|
+
beside the sqlite one). A postgres/s3 rung that can't attach fails loud
|
|
28
|
+
at startup — never a silent fallback to local files.
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
tiers:
|
|
32
|
+
local: { backend: git, root: .canon/ledger }
|
|
33
|
+
hot: { backend: sqlite, path: .canon/hot.db }
|
|
34
|
+
# hot: { backend: postgres, dsn_env: CANON_PG_DSN, schema: canon }
|
|
35
|
+
cold: { backend: s3, bucket_env: CANON_BUCKET, prefix: canon/ }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Using a backend name (`git`/`pg`/`s3`) where a rung is expected is
|
|
39
|
+
rejected with a hint:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
canon.yaml TierPolicy: `git` is a BACKEND name, not a rung — canon.yaml's
|
|
43
|
+
`routing`/`aging`/`tiers` keys now name a capability rung
|
|
44
|
+
(local/hot/cold); declare the backend separately via
|
|
45
|
+
`tiers.<rung>.backend: git`
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Routing a record kind
|
|
49
|
+
|
|
50
|
+
`routing:` is the only place a kind's rung is decided. Keys are the
|
|
51
|
+
stable snake_case kind wire strings (`change`, `evidence_record`,
|
|
52
|
+
`strategy_item`, …) — the same string the record's own `kind` field
|
|
53
|
+
serializes to. Values are `local`/`hot`/`cold`.
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
routing:
|
|
57
|
+
scenario: local # change this to move FUTURE `scenario` writes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Changing a routing value moves future writes only; re-run reads
|
|
61
|
+
resolve the policy live. An unknown kind or rung name is a loud config
|
|
62
|
+
parse error. Every record kind needs a `routing:` entry — an unrouted
|
|
63
|
+
kind fails hard at its first write/query, never silently dropped.
|
|
64
|
+
|
|
65
|
+
## Aging a kind to another rung
|
|
66
|
+
|
|
67
|
+
`aging:` moves records from their routed rung to a `to:` rung once
|
|
68
|
+
their age exceeds `after:`:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
aging:
|
|
72
|
+
handoff: { after: 30d, to: cold } # <n>d/h/m/s — one integer, one unit
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`canon tier age` applies every `aging:` rule once. It is
|
|
76
|
+
content-digest idempotent: re-running finds nothing left to move
|
|
77
|
+
(`moved == 0`); a partial run (destination written, source not deleted)
|
|
78
|
+
re-selects the record but the destination write is a no-op. A kind
|
|
79
|
+
split across two rungs (some aged, some not) still reads correctly —
|
|
80
|
+
`canon query` fans out.
|
|
81
|
+
|
|
82
|
+
## Running `canon tier age` / `canon query`
|
|
83
|
+
|
|
84
|
+
Both take `--canon-yaml <path>` (default `canon.yaml` in the current
|
|
85
|
+
directory).
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Preview what would move (read-only; writes/deletes nothing).
|
|
89
|
+
canon tier age --dry-run
|
|
90
|
+
|
|
91
|
+
# Apply every `aging:` rule once (the destructive move+delete).
|
|
92
|
+
canon tier age
|
|
93
|
+
|
|
94
|
+
# Read a kind, fanning out across its routed rung AND its aging
|
|
95
|
+
# destination, merged by time. --kind is the kind wire string — the
|
|
96
|
+
# same vocabulary as routing:/aging: (`change`, `handoff`, …).
|
|
97
|
+
canon query --kind handoff --since 2026-06-01T00:00:00Z
|
|
98
|
+
|
|
99
|
+
# Machine-readable output instead of the human table.
|
|
100
|
+
canon query --kind trajectory --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## What `canon report` does and doesn't read
|
|
104
|
+
|
|
105
|
+
canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
|
|
106
|
+
builds its numbers from the `mart_` views. Those read ONLY local roots
|
|
107
|
+
— the git ledger and a LOCAL `.canon/r2` parquet mirror. `canon report`
|
|
108
|
+
never opens a live DB connection or the live bucket, so:
|
|
109
|
+
|
|
110
|
+
- Kinds routed to `hot` (sqlite/postgres) do NOT appear in `canon
|
|
111
|
+
report`.
|
|
112
|
+
- Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
|
|
113
|
+
has been separately materialized — there is no automatic sync.
|
|
114
|
+
|
|
115
|
+
These kinds are not lost: read them with `canon query --kind <kind>`.
|
|
116
|
+
The gap is named loud, not under-counted — when any routed kind isn't
|
|
117
|
+
report-readable, `canon report` renders a `## Kinds not read directly`
|
|
118
|
+
section listing them and emits a matching stderr `WARN`.
|
|
119
|
+
|
|
120
|
+
## Retention, export, and access audit
|
|
121
|
+
|
|
122
|
+
`canon purge --kind <kind> --before <RFC3339> [--repo <dir>] [--dry-run] [--json]`
|
|
123
|
+
is the canonical data-lifecycle boundary. The only purgeable kinds are
|
|
124
|
+
`session`, `run`, `event`, and `trajectory`; the cutoff applies to every
|
|
125
|
+
routed rung and every configured aging destination, including retained
|
|
126
|
+
historical versions. For `trajectory`, the command also purges the
|
|
127
|
+
operator-local Parquet learn store under `learn.root` in `canon.yaml`;
|
|
128
|
+
an unavailable configured learn root is an explicit error, never a
|
|
129
|
+
successful zero-row purge. The operation is idempotent. Apply mode
|
|
130
|
+
preflights all attached destinations before the first delete, but storage
|
|
131
|
+
backends are not one transaction: a failure after a prior delete returns
|
|
132
|
+
an explicit `partial` result with matched/deleted counts and a nonzero
|
|
133
|
+
status. Purge never rewrites Git history or deletes source transcript
|
|
134
|
+
files.
|
|
135
|
+
|
|
136
|
+
`canon export --kind <kind> [--before <RFC3339>] [--after <RFC3339>]
|
|
137
|
+
[--out <file>] [--json]` emits a versioned manifest containing source scope,
|
|
138
|
+
counts, digests, and redacted metadata. It does not authorize backend access;
|
|
139
|
+
credentials remain an operator prerequisite. New output files are mode
|
|
140
|
+
`0600`. Raw event detail text is excluded unless `--include-sensitive` is
|
|
141
|
+
passed and `.canon/policy.yaml` explicitly contains:
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
query:
|
|
145
|
+
allow_sensitive: true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Every query, export, and purge appends an actor-stamped record to
|
|
149
|
+
`.canon/audit/access.jsonl` (mode `0600` on creation). `CANON_ACTOR` takes
|
|
150
|
+
precedence over the OS username. Audit rows contain operation, kind, scope,
|
|
151
|
+
sensitive flag, counts, and timestamp only—never query text, secrets, or
|
|
152
|
+
record bodies.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# canon-subject
|
|
2
|
+
|
|
3
|
+
A **Subject** is canon's first-class handle on the durable product unit a
|
|
4
|
+
team plans, designs, builds, verifies, and ships across MANY changes and
|
|
5
|
+
MANY scenarios. This skill covers the subject-write-first contract every
|
|
6
|
+
domain agent follows.
|
|
7
|
+
|
|
8
|
+
## Vocabulary rule (non-negotiable)
|
|
9
|
+
|
|
10
|
+
- **"feature docs" / ".feature files" = Gherkin behavior specs** — the
|
|
11
|
+
`.feature` corpus `canon format`/`canon inventory sync` validate. "Read
|
|
12
|
+
the feature docs" ALWAYS means the Gherkin files, never a Subject.
|
|
13
|
+
- **"subject" = the management record** authored by `canon subject new`.
|
|
14
|
+
A Subject is the product unit; its `.feature` files are its behavior
|
|
15
|
+
specs. Never the same word.
|
|
16
|
+
|
|
17
|
+
A Subject links, but does not contain, its work. Two joins, each
|
|
18
|
+
authored in one place:
|
|
19
|
+
|
|
20
|
+
- **Changes**: `change_ids` on the Subject, appended by `canon subject
|
|
21
|
+
adopt`.
|
|
22
|
+
- **Scenarios**: the `@subject:<id>` tag on each Gherkin scenario,
|
|
23
|
+
indexed onto `Scenario.subject_id` by `canon inventory sync`. The
|
|
24
|
+
Subject record carries no scenario list — the spec corpus is the only
|
|
25
|
+
source, and the Subjects report panel, `spec_coverage.scope`, and the
|
|
26
|
+
shipped gate all read it from there. To link a scenario, tag it and
|
|
27
|
+
re-sync; there is no subject-side command for it.
|
|
28
|
+
|
|
29
|
+
## The 4-step loop
|
|
30
|
+
|
|
31
|
+
Every substantial piece of work starts at a subject write.
|
|
32
|
+
|
|
33
|
+
### 1. Context → author or update the Subject FIRST
|
|
34
|
+
|
|
35
|
+
Write (or update) the Subject before touching anything else, so every
|
|
36
|
+
downstream artifact has a `subject_id` to pin to.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
canon context --repo . # record kinds, enum domains, policy requirements
|
|
40
|
+
canon subject new subject-domain-loop \
|
|
41
|
+
--domain dev --title "Subject domain loop" \
|
|
42
|
+
--summary "The durable product unit + its per-domain loop" \
|
|
43
|
+
--owner-role implementer
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `<id>` is a kebab-case slug (`[a-z0-9]+(-[a-z0-9]+)*`) — the durable
|
|
47
|
+
`subject_id` join key, never renumbered once assigned.
|
|
48
|
+
- `--domain <d>` is checked twice at write: SHAPE (a kebab-case slug),
|
|
49
|
+
then MEMBERSHIP against the domain set THIS repo has activated.
|
|
50
|
+
Canon ships no opinion about how a team slices its work — the set is
|
|
51
|
+
resolved from the vocabulary, so a studio declaring
|
|
52
|
+
`combat`/`economy`/`live-ops` authors against those and canon's base
|
|
53
|
+
set (`planning`, `design`, `dev`, `data`, `test`, from `canon.core`)
|
|
54
|
+
carries no authority there. Extend or replace it in your own
|
|
55
|
+
`.canon/vocab/<id>/enums.yaml` under the `domain` key, activated by a
|
|
56
|
+
`canon.project.yaml` profile (see `canon-vocab`) — never a canon code
|
|
57
|
+
change. A non-member is refused with the legal set named
|
|
58
|
+
(`expected one of: …`), so a typo cannot silently mint a category that
|
|
59
|
+
then splits every `--domain`-filtered read. A repo that declares no
|
|
60
|
+
`domain` enum has no constraint and accepts any slug. Run
|
|
61
|
+
`canon context` to see the set in force.
|
|
62
|
+
- `--summary` is optional; `--owner-role` defaults to `implementer`;
|
|
63
|
+
`--actor-id` defaults to `canon`. A freshly-authored Subject with no
|
|
64
|
+
adopted changes and no tagged scenarios is a valid minimal record and
|
|
65
|
+
starts at status `proposed`. `--json` prints the written record.
|
|
66
|
+
|
|
67
|
+
### 2. Retrieve subject-scoped strategy memory BEFORE working
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `--regime <key>` XOR the derived pair `--domain <d> [--subject <id>]`:
|
|
74
|
+
give the full regime key directly, OR let retrieval derive it. Never both.
|
|
75
|
+
- With `--domain`/`--subject`, retrieval tries `<domain>-<subject_id>`
|
|
76
|
+
first, then `<domain>` — a subject's own lessons win, falling back to
|
|
77
|
+
the domain's shared memory when the subject is new.
|
|
78
|
+
- Fail-soft: an empty/missing store degrades to an empty guidance list,
|
|
79
|
+
never a nonzero exit. See `canon-retrieve` for the full contract.
|
|
80
|
+
|
|
81
|
+
### 3. Work → pin every artifact to `subject_id`
|
|
82
|
+
|
|
83
|
+
- **Scenarios**: `canon scenario new <tag> --subject <subject-id>` emits
|
|
84
|
+
the `@subject:<subject-id>` tag; `canon inventory sync` maps the tag
|
|
85
|
+
onto the scenario's `subject_id`. A malformed/absent tag leaves it
|
|
86
|
+
unset (fail-soft), never an error. See `canon-authoring`.
|
|
87
|
+
- **Changes**: adopt imported plan changes under the Subject (see
|
|
88
|
+
[Adopt flow](#adopt-flow)).
|
|
89
|
+
- **Reviews / evidence / divergences**: authored as usual (`canon review
|
|
90
|
+
add`, `canon gate`, `canon divergence …`) against the Subject's
|
|
91
|
+
scenarios — the join spine carries them back through `scenario_id`.
|
|
92
|
+
|
|
93
|
+
### 4. Verdicts / trajectories → learn (knowledge out)
|
|
94
|
+
|
|
95
|
+
As work is reviewed, verdicts ingest under the subject-scoped regime
|
|
96
|
+
(`canon ingest artifacts`), so `canon learn` distills strategies keyed to
|
|
97
|
+
this subject and domain — the exact memory step 2 retrieves next time.
|
|
98
|
+
|
|
99
|
+
## Status lifecycle + the shipped evidence gate
|
|
100
|
+
|
|
101
|
+
`canon subject status <id> <state>` performs a policy-gated transition.
|
|
102
|
+
The legal chain:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
proposed → specced → building → verifying → shipped
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
plus **any state → retired** (a subject can be retired from anywhere).
|
|
109
|
+
Any other transition is rejected by failure class, exits 1, and leaves
|
|
110
|
+
the record UNCHANGED (fail closed).
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
canon subject status subject-domain-loop specced
|
|
114
|
+
canon subject status subject-domain-loop building
|
|
115
|
+
canon subject status subject-domain-loop verifying
|
|
116
|
+
canon subject status subject-domain-loop shipped # gated — see below
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
On success the status updates in place; `--json` prints the updated
|
|
120
|
+
record. On a gate block it prints violations by failure class to stderr,
|
|
121
|
+
exits 1, and the record is unchanged.
|
|
122
|
+
|
|
123
|
+
**Corpus-wide, the same join:** `canon gate check` with
|
|
124
|
+
`.canon/policy.yaml`'s opt-in `spec_coverage` section generalizes this
|
|
125
|
+
gate past one transition — it left-joins the WHOLE Scenario corpus
|
|
126
|
+
against evidence, so an unimplemented or mismatched spec is reported
|
|
127
|
+
whether or not a Subject links it. `spec_coverage.scope` narrows
|
|
128
|
+
blocking to subjects in named statuses, and `@subject:<id>` tagging is
|
|
129
|
+
what brings a scenario into that scope. See `canon-gate`.
|
|
130
|
+
|
|
131
|
+
**The `verifying → shipped` evidence gate:** shipping additionally
|
|
132
|
+
requires that the Subject OWNS at least one scenario (one whose latest
|
|
133
|
+
synced generation carries `@subject:<id>`), and that EVERY owned
|
|
134
|
+
scenario carries a latest, non-`Divergent` verdict in the ledger (the
|
|
135
|
+
same last-wins rule `canon gate check` uses). A Subject with no tagged
|
|
136
|
+
scenario is refused rather than shipped on an empty set; so is one whose
|
|
137
|
+
`scenario` records route away from the gate's rung. Each refusal prints
|
|
138
|
+
by failure class (`uncovered-cell`), exits 1, and the status stays
|
|
139
|
+
`verifying`. `retired` is not gated.
|
|
140
|
+
|
|
141
|
+
## Adopt flow
|
|
142
|
+
|
|
143
|
+
Planning docs become change/task records via `canon ingest plans` (see
|
|
144
|
+
`canon-plan-import`), but that stops at import. `canon subject adopt`
|
|
145
|
+
lifts an imported change into a managed Subject:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
canon ingest plans --repo . # import → change/task rows
|
|
149
|
+
canon subject adopt subject-domain-loop-plan \
|
|
150
|
+
--subject subject-domain-loop # link change → subject
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`adopt` stamps the change's `subject_id` and adds it to the Subject's
|
|
154
|
+
`change_ids`, so `canon query --kind change --change-id …` and `canon
|
|
155
|
+
query --kind subject` agree on the link from both ends.
|
|
156
|
+
|
|
157
|
+
## Reading the per-domain management view
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
canon query --kind subject --domain dev --status building
|
|
161
|
+
canon report --repo . # renders the "Subjects" panel
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `canon query --kind subject [--domain <d>] [--status <s>]` — the
|
|
165
|
+
per-domain management view; `--domain`/`--status` are subject-only
|
|
166
|
+
filters.
|
|
167
|
+
- `canon report`'s **Subjects** panel is a per-domain rollup: one row per
|
|
168
|
+
subject (`domain`, `subject_id`, `title`, `status`, `scenario_count`,
|
|
169
|
+
`covered_scenarios`), where `covered_scenarios` counts linked scenarios
|
|
170
|
+
carrying a latest non-`Divergent` verdict — the same coverage the
|
|
171
|
+
shipped gate enforces, surfaced read-only. Also exported by `canon
|
|
172
|
+
report --snapshot` (see `canon-report-dashboard`).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# canon-vocab
|
|
2
|
+
|
|
3
|
+
Canon's typed authoring layer replaces a freeform `tasks.md` checkbox's
|
|
4
|
+
implicit evidence with a structured, checker-validated, policy-resolved
|
|
5
|
+
attribute — and gives handoff bodies a declared, rendered template. You
|
|
6
|
+
declare a vocabulary in a `.canon/vocab/<id>/` plugin, then author atoms
|
|
7
|
+
that the checker validates against it.
|
|
8
|
+
|
|
9
|
+
## Run `canon context` FIRST
|
|
10
|
+
|
|
11
|
+
Before authoring any atom, run `canon context` (or `--json`) and read its
|
|
12
|
+
`vocab` section: it lists every declarable directive + enum for the repo
|
|
13
|
+
plus a content-hash version. Authoring against a directive/enum
|
|
14
|
+
`canon context` does not list means the checker will reject it — the
|
|
15
|
+
surface is the source of truth for "what can I declare here?" See
|
|
16
|
+
`canon-context`.
|
|
17
|
+
|
|
18
|
+
## Declaring a directive or enum
|
|
19
|
+
|
|
20
|
+
A vocabulary plugin lives at `.canon/vocab/<id>/`:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# .canon/vocab/<id>/plugin.yaml
|
|
24
|
+
id: my.plugin
|
|
25
|
+
version: "0.1.0"
|
|
26
|
+
kind: project # `core` for canon.core; `project` for a consumer plugin
|
|
27
|
+
exports:
|
|
28
|
+
directives: directives/ # a dir of *.yaml directive files
|
|
29
|
+
enums: enums.yaml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# .canon/vocab/<id>/directives/thing.yaml
|
|
34
|
+
directives:
|
|
35
|
+
- name: thing
|
|
36
|
+
attrs:
|
|
37
|
+
- name: desc
|
|
38
|
+
type: string # scalar
|
|
39
|
+
required: true
|
|
40
|
+
- name: status
|
|
41
|
+
type: { domain: task-status } # value must be a member of the enum
|
|
42
|
+
required: true
|
|
43
|
+
- name: tags
|
|
44
|
+
type: { list: string } # a YAML list of that element type
|
|
45
|
+
required: false
|
|
46
|
+
- name: evidence
|
|
47
|
+
type: evidence # SPECIAL: {kind, ref}; `kind`'s domain
|
|
48
|
+
required: true # resolves LIVE from .canon/policy.yaml,
|
|
49
|
+
# never a locally-declared enum
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# .canon/vocab/<id>/enums.yaml
|
|
54
|
+
enums:
|
|
55
|
+
task-status: [open, done]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Type kinds**: `string`; `{ domain: <enum> }` (validated against
|
|
59
|
+
`enums.yaml`); `{ list: <type> }`; and `evidence` (the `{kind, ref}`
|
|
60
|
+
shape whose `kind` domain comes from `.canon/policy.yaml`'s evidence
|
|
61
|
+
kinds, NOT a local enum).
|
|
62
|
+
- The evidence-kind domain is DELIBERATELY absent from `enums.yaml` — do
|
|
63
|
+
not add it there; it resolves dynamically from policy.
|
|
64
|
+
|
|
65
|
+
## Authoring a typed task atom
|
|
66
|
+
|
|
67
|
+
An atom file is a YAML list of `{ id, tag, attrs }` records. A task atom
|
|
68
|
+
tags `task` and must satisfy the `task` directive's attrs:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
- id: my-change#12.3 # <change_id>#<n> (the task_id join key)
|
|
72
|
+
tag: task
|
|
73
|
+
attrs:
|
|
74
|
+
desc: "wire the checker against the real manifest"
|
|
75
|
+
owner: alice # optional
|
|
76
|
+
status: open # must be a task-status enum member
|
|
77
|
+
evidence:
|
|
78
|
+
kind: test-run # must be a policy.yaml evidence kind
|
|
79
|
+
ref: "cargo test"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The atom is validated against the resolved vocabulary and compiled to a
|
|
83
|
+
canon `Task`. A wrong tag, an unknown attr, a missing required attr, an
|
|
84
|
+
out-of-domain `status`, or an evidence `kind` absent from `policy.yaml`
|
|
85
|
+
each produces a checker diagnostic (the "expected one of: …" shape) — the
|
|
86
|
+
atom is rejected at author time, never compiled into a malformed Task.
|
|
87
|
+
|
|
88
|
+
## Authoring a vocabulary-defined handoff body
|
|
89
|
+
|
|
90
|
+
A handoff body tags `handoff-<domain>` (the domain selects the directive,
|
|
91
|
+
NOT an attr value) and satisfies that directive's attrs:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
- id: my-handoff-1
|
|
95
|
+
tag: handoff-dev # dev | design | content | test (canon.core)
|
|
96
|
+
attrs:
|
|
97
|
+
title: "wire the vocabulary into the CLI"
|
|
98
|
+
summary: "the foundation is ready for CLI integration"
|
|
99
|
+
verification-steps:
|
|
100
|
+
- "cargo build"
|
|
101
|
+
- "cargo test"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It's validated and compiled to a canon handoff body. Each
|
|
105
|
+
`handoff-<domain>` directive declares its own attrs (e.g. `design` /
|
|
106
|
+
`content` domains may require `acceptance-criteria` instead of
|
|
107
|
+
`verification-steps`) — read the domain's directive (or `canon context`)
|
|
108
|
+
for the exact required set.
|
|
109
|
+
|
|
110
|
+
## What this skill does NOT cover
|
|
111
|
+
|
|
112
|
+
- The `canon context` surface itself — see `canon-context`; this skill
|
|
113
|
+
covers authoring AGAINST it.
|
|
114
|
+
- `policy.yaml` / CEL evidence-requirement authoring — see `canon-policy`;
|
|
115
|
+
here you only reference an evidence `kind` policy already declares.
|