@erclx/canon 4.119.0 → 4.120.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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-skill/SKILL.md +2 -1
  4. package/claude/skills/git-pr/REQUIREMENT.md +1 -0
  5. package/claude/skills/git-pr/SKILL.md +34 -4
  6. package/claude/skills/review-pr/SKILL.md +13 -242
  7. package/claude/skills/review-pr/references/close-out.md +30 -0
  8. package/claude/skills/review-pr/references/post.md +145 -0
  9. package/claude/skills/review-pr/references/scope.md +82 -0
  10. package/claude/skills/role-orchestrator/REQUIREMENT.md +9 -16
  11. package/claude/skills/role-orchestrator/SKILL.md +9 -153
  12. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -193
  13. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +22 -0
  14. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +85 -0
  15. package/claude/skills/role-orchestrator/references/orchestrator-output.md +61 -0
  16. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  17. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +56 -0
  19. package/claude/skills/role-orchestrator/references/orchestrator-review-fallback.md +18 -0
  20. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +1 -1
  21. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  22. package/docs/agents/commands.md +2 -1
  23. package/docs/agents/counts.md +1 -1
  24. package/docs/agents/index.md +8 -3
  25. package/docs/agents/pr-evidence.md +19 -3
  26. package/docs/agents/pr-local.md +102 -0
  27. package/docs/agents/pr-preview.md +2 -2
  28. package/docs/agents/records-push.md +61 -0
  29. package/docs/agents/records-validate.md +73 -0
  30. package/docs/agents/records.md +3 -119
  31. package/docs/agents/sessions-transfer.md +91 -0
  32. package/docs/agents/sessions.md +3 -82
  33. package/docs/agents/skills-audit.md +2 -2
  34. package/docs/agents/teach-authoring.md +112 -0
  35. package/docs/agents/teach.md +3 -103
  36. package/docs/target-projects.md +1 -1
  37. package/docs/workflow/ai-workflow.md +1 -1
  38. package/docs/workflow/visual-design-workflow.md +1 -1
  39. package/governance/rules/claude/570-skill.md +2 -1
  40. package/package.json +1 -1
  41. package/scripts/core/check-ignore-parity.sh +1 -1
  42. package/scripts/lib/sandbox-git.sh +1 -1
  43. package/src/commands/pr.ts +324 -2
  44. package/src/context/classify/extract.ts +2 -2
  45. package/src/counts/scan.ts +1 -1
  46. package/src/design/tokens.ts +1 -1
  47. package/src/gate/stages.ts +2 -2
  48. package/src/pr/evidence.ts +42 -2
  49. package/src/pr/local.ts +125 -0
  50. package/standards/index.md +1 -0
  51. package/standards/skill-requirement.md +72 -0
  52. package/standards/skill.md +34 -152
  53. package/tooling/astro/reference.md +1 -1
  54. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +32 -0
@@ -31,16 +31,20 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
31
31
  - [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
32
32
  - [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
33
33
  - [The pull request evidence comparison](pr-evidence.md): What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, and why the comparison anchors at the merge base rather than the previous push
34
+ - [The pull request local preview](pr-local.md): How canon pr local finds the server a worktree is running, why a server in another worktree is never reported, the refusal reasons it names, and how a close workflow removes the link with --remove
34
35
  - [The pull request preview deploy](pr-preview.md): How canon pr preview finds a fenced Cloudflare Pages deploy, dispatches it on a pull request's branch, reads the alias the run prints, the refusal reasons it names, and why an unfenced deploy is refused before anything runs
35
36
  - [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
36
- - [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
37
+ - [Records push](records-push.md): Backing the record folders to a private remote and restoring them, which folders the payload carries, the one-time setup, the refusal reasons, and the hook that runs the push
38
+ - [Records validate](records-validate.md): Validating the session records and the standards corpus, the per-kind checks, which root each kind defaults to, the exit codes, and the refusal reasons
39
+ - [Records](records.md): The two roots a record folder resolves at, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, pruning stale scratch, and where validation and the private-remote backup are described
37
40
  - [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
38
41
  - [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
39
42
  - [Routing report](routing.md): Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
40
43
  - [Rule citations](rule-citations.md): Resolving every path a governance rule cites and every frontmatter glob the internal corpus declares, the three forms a citation is written in, the shapes that look like citations and are not, the two classes where an absent path is correct, why the glob half reads one corpus, and why this one gates
41
44
  - [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
42
45
  - [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
43
- - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, what each session surface can see, and moving a session to another machine with export and import
46
+ - [Sessions transfer](sessions-transfer.md): Moving a session to another machine, the export bundle and its manifest, placing it with import, the refusal reasons, and what a resume on the target depends on
47
+ - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, what each session surface can see, and where moving a session to another machine is described
44
48
  - [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
45
49
  - [Citation reach](skills-reach.md): Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
46
50
  - [Standard success criteria](standards-audit.md): Reading the corpus against the success-criterion gate, the two headings it accepts, why the check scopes to arrival rather than the whole corpus, and the exit codes it sets
@@ -48,6 +52,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
48
52
  - [Superseded values](superseded.md): Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the family stem behind a templated citation, the exemption marker, the blind spots it cannot reach, and why it reports rather than gates
49
53
  - [Targets](targets.md): The projects this toolkit installed into, the record the install writes against the sweep that backs it, what bounds each answer, and the cross-target pull request read
50
54
  - [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, deriving the branch a dispatch and a worker both take, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
51
- - [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, rewriting the root listing, a contents page, and each lesson's chrome and quiz stepper from its marker regions, the refusal reasons, and why every write here runs through a verb
55
+ - [Teach authoring](teach-authoring.md): Resolving what the next lesson needs before it is written, rendering a lesson body's blocks to HTML, and rewriting the root listing, a contents page, and each lesson's chrome and quiz stepper from its marker regions
56
+ - [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
52
57
  - [Test order](test-order.md): Reading where an implementation reached history ahead of its test, how a pair is decided, the three verdicts, the coverage the pairing cannot reach, and why the check reports rather than gates
53
58
  - [Worktrees](worktrees.md): Reporting which worktrees are reclaimable, removing the ones that are, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
@@ -122,14 +122,30 @@ already holds, the same way the preview address is carried. That is what keeps
122
122
  already ticked, since `git-pr` deletes the handoff once the first post reports
123
123
  success and no later call has a file to pass.
124
124
 
125
- The option does not decide `no-evidence`. A branch with a checklist and no
126
- changed evidence image still reports `no-evidence`, and the caller posts the
127
- checklist on its own, which is what it did before this option existed. A path
125
+ The option does not decide `no-evidence` alone. A branch with a checklist and
126
+ no changed evidence image still reports `no-evidence`, and the caller posts the
127
+ checklist on its own, which is what it did before this option existed. A
128
+ checklist passed together with `--local` is the exception, covered below. A path
128
129
  that cannot be read, or one holding nothing, refuses as
129
130
  `unreadable-checklist` rather than rendering without it: the caller deletes the
130
131
  handoff once a post succeeds, so a silently dropped checklist is the only copy
131
132
  gone.
132
133
 
134
+ ## The local preview line
135
+
136
+ `--local <url>` adds `**Local preview:** <url>` to the opening block: under the
137
+ hosted `**Preview:**` line when there is one, and as the first line when there
138
+ is not. The hosted line always keeps the first position, since that is the one
139
+ line the carry-forward reads for it. A run without the option carries forward
140
+ the local address the marked comment already holds, the same as the other two.
141
+
142
+ The option does not turn `no-evidence` into `ok` on its own. A branch with a
143
+ dev server running and no rendered change would otherwise get a comment holding
144
+ a link and nothing else. Together with `--checklist` it does, so the link and
145
+ the checklist land in one comment rather than the checklist going out alone.
146
+ `canon docs pr-local` covers where the address comes from and how the line is
147
+ removed at close.
148
+
133
149
  ## What a collapsed comment still leaves to GitHub
134
150
 
135
151
  GitHub already draws its own before-and-after comparison on the Files Changed
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: The pull request local preview
3
+ description: How canon pr local finds the server a worktree is running, why a server in another worktree is never reported, the refusal reasons it names, and how a close workflow removes the link with --remove
4
+ ---
5
+
6
+ # The pull request local preview
7
+
8
+ `canon pr local` reports the `localhost` address of the dev server the current
9
+ worktree is running. `git-pr` calls it ahead of the evidence comparison and
10
+ passes the address through `canon pr evidence --local`, so a reviewer on the
11
+ same machine gets the running branch to click into beside the screenshots and
12
+ the checklist. A workflow on the pull request's `closed` event calls it again
13
+ with `--remove`, which replaces the line with a note.
14
+
15
+ ```bash
16
+ canon pr local --json
17
+ canon pr local 1341 --remove --json
18
+ ```
19
+
20
+ ## How it finds the server
21
+
22
+ The verb reads every TCP socket in `LISTEN` with the process that owns it. It
23
+ keeps a socket when that process's working directory sits inside the
24
+ worktree's toplevel, including a subfolder, since some tools change into one
25
+ before they listen. It then requests `/` on each kept port and reports the
26
+ lowest one serving an HTML page.
27
+
28
+ The HTML test is there because a working tree often holds more than one
29
+ listener. A test runner's own server, or a dev server's reload socket, answers
30
+ HTTP too, and a reviewer opening either gets a bare 404 rather than the branch.
31
+
32
+ Sockets are read through `lsof` where it is installed. A Linux machine without
33
+ it falls back to `/proc/net/tcp` and `/proc/net/tcp6`, matched against each
34
+ process's `fd` table. A machine with neither refuses as `no-listener-reader`.
35
+
36
+ The port is never derived from the worktree. A port helper needs a per-stack
37
+ base port the toolkit cannot know, and a server started by hand or by an
38
+ earlier session would not match it anyway.
39
+
40
+ ## Why another worktree's server is never reported
41
+
42
+ A server running in a different worktree shows a different branch, and a link
43
+ to it on this pull request would show a reviewer the wrong code.
44
+
45
+ - A sibling linked worktree sits outside this one's toplevel, so its process
46
+ never matches.
47
+ - The main checkout's server, read from a linked worktree, sits outside the
48
+ linked worktree's toplevel for the same reason.
49
+ - A linked worktree's server, read from the main checkout, sits inside the main
50
+ toplevel. The verb excludes everything under `.claude/worktrees/` for that
51
+ case alone.
52
+
53
+ ## What the record carries
54
+
55
+ `reason` on the record is what a caller branches on, not the exit code:
56
+
57
+ | Reason | What it means |
58
+ | -------------------- | --------------------------------------------------------------------- |
59
+ | `ok` | A server answered. `url` carries the address and `port` the port. |
60
+ | `no-server` | Nothing inside this worktree served an HTML page. |
61
+ | `no-listener-reader` | Neither `lsof` nor `/proc` is available, so no socket could be read. |
62
+ | `removed` | `--remove` replaced the local line on the evidence comment. |
63
+ | `no-comment` | `--remove` found no comment carrying the evidence marker. |
64
+ | `no-line` | `--remove` found the comment but no local line on it. |
65
+ | `gh-missing` | `--remove` needs `gh` and it is not on the path. |
66
+ | `gh-failed` | `--remove` could not read the comments or could not edit the comment. |
67
+
68
+ The exit is 0 on `ok` and on every `--remove` outcome that finished, including
69
+ the two no-ops, and 1 on every refusal. A caller on an older binary gets an
70
+ unknown-subcommand error instead of a record, which `git-pr` reads as a skip.
71
+
72
+ ## Removing the link at close
73
+
74
+ The link goes dead once the branch's server stops, and nothing on the pull
75
+ request says so. `--remove` runs on the pull request's `closed` event, merged or
76
+ abandoned, from a workflow seeded into every target:
77
+
78
+ ```yaml
79
+ on:
80
+ pull_request:
81
+ types: [closed]
82
+ ```
83
+
84
+ It reads the comment carrying the `pr-evidence` marker, replaces the
85
+ `**Local preview:**` line with the `--note` text, and edits the comment itself
86
+ through `gh api`. The verb writes here rather than handing a body back, because
87
+ its caller is a workflow with no session to post one. Every other byte of the
88
+ comment stays as it was, which keeps the marker, the checklist delimiters, and
89
+ any ticked box intact. A second run finds no line and reports `no-line`.
90
+
91
+ A fork pull request gets a read-only token, so the workflow skips the edit with
92
+ a notice rather than failing the close.
93
+
94
+ ## What this does not cover
95
+
96
+ The link can still go dead while the pull request is open, such as when the
97
+ session holding the server retires. Only the close is covered, and a re-render
98
+ after a push carries the line forward whether the server still answers or not.
99
+
100
+ The hosted `**Preview:**` line from `canon pr preview` stays on the comment
101
+ after close, even though its deployments are deleted then. `canon docs
102
+ pr-preview` covers that path.
@@ -100,5 +100,5 @@ request never deploys.
100
100
  ## What this does not cover
101
101
 
102
102
  The verb builds only the deployed-and-public case. A private repository whose
103
- previews need access protection, and a project with no deploy at all where a
104
- local server would stand in, both wait for a project that needs them.
103
+ previews need access protection waits for a project that needs it. A local
104
+ server stands in where there is no deploy, through `canon docs pr-local`.
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Records push
3
+ description: Backing the record folders to a private remote and restoring them, which folders the payload carries, the one-time setup, the refusal reasons, and the hook that runs the push
4
+ ---
5
+
6
+ # Records push
7
+
8
+ `canon records push` commits the backed record folders to a private remote and pushes them. `canon records pull` fetches the other direction and writes them back. Both take `--json` and `--root` the way `validate` does, and both exit `0` on agreement and `1` on a refusal.
9
+
10
+ ```bash
11
+ canon records push
12
+ canon records push --json
13
+ canon records pull
14
+ ```
15
+
16
+ At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The report also lists every file new to the history under `added`, since a stray file inside a folder already tracked is visible only by name.
17
+
18
+ The three excluded names bound the records index as well as the disk. A name an older binary committed, such as `tmp`, is removed from the history once and reported under `dropped`, and no later push stages it again. That removes it from the tip going forward and purges nothing from earlier commits the remote already holds. A project's own drafts folder needs no config to be backed: moved to `.canon/<name>/`, it is a top-level directory like any other and enters the payload on the next push. The legacy `.claude` root keeps a fixed allowlist instead, since that root also holds tracked `skills/`, `rules/`, and `hooks/` a push must never carry, and an exclusion set there would stage all three. Each name is a top-level record folder and every archive sits inside the one it archives, so the set stays at one entry per surface however many archives appear, and it deliberately does not match the six record kinds `validate` hardcodes.
19
+
20
+ Records are gitignored by design, so the history lives in a second git directory at `.records.git` inside the record root, with that root as its work tree. Both resolve off the root together rather than folder by folder, since a history opened at one root beside a work tree at the other would stage the deletion of every folder a move relocated. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the backed folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Each pathspec is a bare folder name and git reads it against the current directory rather than against the work tree the same call names, so the invocation carries `-C` at the work tree beside the other two flags. That is what lets either verb run from a linked worktree under `.claude/worktrees/`, which sits inside the records work tree and would otherwise prefix every name with its own path.
21
+
22
+ ## Setup
23
+
24
+ A person creates the records repository once per machine, and the verbs refuse with the commands when it is absent:
25
+
26
+ ```bash
27
+ git --git-dir=.canon/.records.git init
28
+ git --git-dir=.canon/.records.git remote add origin <private-repo-url>
29
+ printf '.canon/\n' >> .gitignore
30
+ ```
31
+
32
+ The ignore line is repeated here rather than left to the install, because the person running these commands is the one who creates the directory and the rule is worth reading beside the command that needs it. The claude manifest ships `.canon/` too, so a project that ran `canon tooling sync` already carries it and this line is a no-op there. One root entry covers the history and every record beside it, which is what makes the rule worth stating once rather than per folder.
33
+
34
+ Point it at a private repository, and at one that is not a remote of the project. Records carry the memory pen, the review reports, and the groundwork trails, so a public project publishes all of it to anyone who fetches all refs. `push` compares the configured origin against every remote of the project and refuses on a match. A read of that list which fails refuses as well, since an empty list clears the comparison for every origin and a gate that passes on its own failure is no gate.
35
+
36
+ ## Refusals
37
+
38
+ | Reason | What fired |
39
+ | ------------------- | -------------------------------------------------------------------------------------- |
40
+ | `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
41
+ | `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
42
+ | `no-remote` | The records history has no `origin` |
43
+ | `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
44
+ | `remote-shared` | The records origin is also a remote of the project |
45
+ | `no-remote-records` | `pull` found no branch on the records origin |
46
+ | `local-changes` | `pull` found records on disk that the history does not carry |
47
+ | `local-ahead` | `pull` found local commits that never reached the origin |
48
+ | `unsafe-payload` | `push` found a pending file over 25 MB or carrying a credential, named under `blocked` |
49
+ | `git-failed` | A git call failed, with its stderr in the message |
50
+
51
+ `split-roots` runs ahead of every gate below it and fires on a half-migrated tree, which is what a `canon migrate records` run that failed partway leaves. `recordRoot` answers for the whole tree on the first root that exists, so a folder left at the old root is absent from the work tree while the records index still names it, and an unguarded `add -A` would stage its deletion and drop it from the remote on the next push. Finish the move, or put the stranded folders back beside the others.
52
+
53
+ `unsafe-payload` runs before anything is staged, so a refusal leaves the index, the log, and the object store as it found them. It reads every new or changed file the push would stage and blocks one over 25 MB, which is read off the file's size without opening it, or one carrying a value `canon secrets scan` would report, through the same patterns and the same `canon-allow-secret` marker. It refuses the whole push rather than skipping the file, and `--json` lists each path with its cause and a detail that names the credential's kind and line, never its value. Move the file out of the record folders, such as into `.canon/tmp/`, or remove the credential, then push again. The guard reads only what this push would add, so a credential committed before it existed stays in the history unreported.
54
+
55
+ A push the remote rejects undoes the commit that run made and leaves the records on disk, so the next run does not send a payload the remote already refused. A history an older binary committed and left unpushed is outside that undo, and the guard never reads it either, so every push sends the same blob again. Reset it by hand from the project root to the last commit the origin holds, `git --git-dir=.canon/.records.git --work-tree=.canon reset <commit>`, and push again. The `--work-tree` is required, since the setup `init` marks the history bare and git refuses that reset on a bare repository.
56
+
57
+ The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding no backed folders has nothing to lose, so a restore onto a fresh checkout runs straight through.
58
+
59
+ ## When it runs
60
+
61
+ `.husky/post-merge` runs `push` after the task archiving loop, on every merge rather than only on one that archived a task. A review report and a memory entry both land on runs that close nothing. The call sits inside an `if` and last in the file, so an unreachable remote neither aborts the hook nor delays the archiving above it, and a checkout that never ran the setup reports nothing. Anything the hook misses is covered by running the verb by hand.
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Records validate
3
+ description: Validating the session records and the standards corpus, the per-kind checks, which root each kind defaults to, the exit codes, and the refusal reasons
4
+ ---
5
+
6
+ # Records validate
7
+
8
+ `canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under the record root, resolved at `.canon/` first: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
9
+
10
+ ```bash
11
+ canon records validate plans
12
+ canon records validate memory
13
+ canon records validate standards
14
+ canon records validate intake --json
15
+ ```
16
+
17
+ | Option | Behavior |
18
+ | --------------- | ------------------------------------------------------------------- |
19
+ | `--json` | Add a machine-readable record on stdout |
20
+ | `--root <path>` | Project root, defaulting to the main worktree except on `standards` |
21
+
22
+ It reads and never writes, and the reason splits by kind. A session record is per-machine scratch with no history behind it, so a repair that guessed wrong could not be undone. A standard installs into every target and is cited by bare filename, so a rename the verb performed would reach further than the file it moved, which is why a finding naming one says so.
23
+
24
+ `standards` reads the authoring root at `standards/` where it exists and a copy at `.claude/standards/` otherwise. The authoring root wins because it is the only tree anyone authors in and the only one the resolver answers from, so a finding fixed anywhere else is fixed where nothing reads it. No repository generates the second candidate any more, which leaves it as a floor under a target holding a copy an older toolkit installed. The walk stays flat, matching the catalog.
25
+
26
+ Nothing fires it automatically. The five record folders are gitignored, so the standards-audit hook exits early on them and any check reading changed files from git never lists one. The corpus is tracked and still unreached, since the markdown audit reads content across the files git lists and rules on no filename. The verb runs at the moment a session claims the record is finished, which is the same placement `canon tasks validate` takes over the board.
27
+
28
+ The two record roots every kind resolves at are in `records.md`.
29
+
30
+ ## What each kind checks
31
+
32
+ | Kind | What it reports |
33
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one, a question carrying no suggestion or no answer slot, and a batch still staged with a `**Batch N**` sub-heading inside the one file rather than split into its own plan |
35
+ | `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
36
+ | `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
37
+ | `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part |
38
+ | `standards` | A standard missing `title` or `description`, an absent `## Scope` section, a scope section carrying no `Does not govern:` list, a statement anchoring nothing, and a filename naming no part of the path the statement governs |
39
+ | `teach` | A workspace folder carrying no two-digit ordinal, an absent `MISSION.md`, `RESOURCES.md`, or `GLOSSARY.md`, a file missing `title` or `description`, a mission with no `date` as `YYYY-MM-DD` or no `## Success looks like` section, an unnumbered learning record, and a reference page opening with an ordinal |
40
+
41
+ The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
42
+
43
+ The item check skips `00-overview.md` and `99-next-session.md`, since neither holds items and running it over the handoff would report every heading it carries. The memory walk skips `index.md` for the same reason, since the catalog is generated from its siblings rather than authored as an entry.
44
+
45
+ A memory `category` is compared against the sentence-case form of the filename prefix rather than checked field by field, so one finding covers a prefix outside the four types, a field disagreeing with the prefix, and a casing drift that would open a second group in the catalog. The body check runs on `feedback` and `project` entries alone, because a `user` or `reference` entry is a single sentence by design and has no rule to apply.
46
+
47
+ The standards filename check derives a word from the governed path rather than counting words in the name. Every member of the corpus is named for the artifact its scope statement governs, and one word is what that produces rather than the rule itself, so a check keyed on word count would pass a conforming single word naming the wrong artifact. Each path segment offers its own word, a dotted container segment offers none, a hyphenated segment offers its parts, and the singular and the plural both match.
48
+
49
+ The scope statement is read exactly as `scripts/standards/list.sh` reads it for the catalog's `appliesTo` field: backticked spans in the first sentence alone, with an attribute standard resolving where that sentence backticks nothing and the statement says it governs an attribute. One sentence read two ways would let a standard pass the check while publishing a different jurisdiction to every consumer of the catalog. An attribute standard is exempt from the filename derivation because it governs no path to derive from, and a statement that anchors nothing and claims no attribute is reported as `scope-unanchored` rather than passed, since silence there would let the backticks be removed to disable the check.
50
+
51
+ An absent `## Scope` section suppresses the filename finding. The name derives from the statement, so a missing section leaves nothing to derive against, and reporting both would name one defect twice and point the fix at the wrong file. The walk skips `index.md`, which is generated from its siblings rather than authored.
52
+
53
+ A plan section opens as a bold label or as an H2 and the check counts both, naming the standard's spelling when it reports one missing. The corpus splits roughly four to one between the two forms, so failing the variant would report nearly every plan on a rule that costs a reader nothing.
54
+
55
+ A section runs to the next marker-shaped line whatever it names, so a plan carrying a label of its own closes the section above it rather than collecting into it. Fenced blocks are dropped before any of this, since a plan showing the shape it writes puts real-looking bullets and headings inside a fence.
56
+
57
+ ## Exit codes and refusals
58
+
59
+ Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so a record kind's refusal names both record roots and the `standards` refusal names the authoring root and the installed copy.
60
+
61
+ An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `findings` array and its `reason` rather than the exit when a skill consumes this.
62
+
63
+ The four record folders are shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. A linked worktree validates the same records every other session reads.
64
+
65
+ `standards` takes the other default, the root of the checkout the caller stands in. The corpus is tracked, so a linked worktree holds its own edited copy, and resolving the main root there would report on a tree the session never touched while saying nothing about which one it read. A session that adds or renames a standard inside a worktree is the case, and it is the one the check exists for.
66
+
67
+ Skills branch on the findings rather than on the exit code:
68
+
69
+ ```bash
70
+ canon records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
71
+ ```
72
+
73
+ For the shapes each check enforces, see `standards/plan.md`, `standards/groundwork.md`, `standards/intake.md`, `standards/memory.md`, and `standards/standard.md`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Records
3
- description: The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
3
+ description: The two roots a record folder resolves at, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, pruning stale scratch, and where validation and the private-remote backup are described
4
4
  ---
5
5
 
6
6
  # Records
@@ -17,70 +17,7 @@ A refusal names every root it looked at, so a message reading `no-folder` says w
17
17
 
18
18
  ## Validate
19
19
 
20
- `canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under the record root, resolved at `.canon/` first: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
21
-
22
- ```bash
23
- canon records validate plans
24
- canon records validate memory
25
- canon records validate standards
26
- canon records validate intake --json
27
- ```
28
-
29
- | Option | Behavior |
30
- | --------------- | ------------------------------------------------------------------- |
31
- | `--json` | Add a machine-readable record on stdout |
32
- | `--root <path>` | Project root, defaulting to the main worktree except on `standards` |
33
-
34
- It reads and never writes, and the reason splits by kind. A session record is per-machine scratch with no history behind it, so a repair that guessed wrong could not be undone. A standard installs into every target and is cited by bare filename, so a rename the verb performed would reach further than the file it moved, which is why a finding naming one says so.
35
-
36
- `standards` reads the authoring root at `standards/` where it exists and a copy at `.claude/standards/` otherwise. The authoring root wins because it is the only tree anyone authors in and the only one the resolver answers from, so a finding fixed anywhere else is fixed where nothing reads it. No repository generates the second candidate any more, which leaves it as a floor under a target holding a copy an older toolkit installed. The walk stays flat, matching the catalog.
37
-
38
- Nothing fires it automatically. The five record folders are gitignored, so the standards-audit hook exits early on them and any check reading changed files from git never lists one. The corpus is tracked and still unreached, since the markdown audit reads content across the files git lists and rules on no filename. The verb runs at the moment a session claims the record is finished, which is the same placement `canon tasks validate` takes over the board.
39
-
40
- ### What each kind checks
41
-
42
- | Kind | What it reports |
43
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one, a question carrying no suggestion or no answer slot, and a batch still staged with a `**Batch N**` sub-heading inside the one file rather than split into its own plan |
45
- | `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
46
- | `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
47
- | `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part |
48
- | `standards` | A standard missing `title` or `description`, an absent `## Scope` section, a scope section carrying no `Does not govern:` list, a statement anchoring nothing, and a filename naming no part of the path the statement governs |
49
- | `teach` | A workspace folder carrying no two-digit ordinal, an absent `MISSION.md`, `RESOURCES.md`, or `GLOSSARY.md`, a file missing `title` or `description`, a mission with no `date` as `YYYY-MM-DD` or no `## Success looks like` section, an unnumbered learning record, and a reference page opening with an ordinal |
50
-
51
- The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
52
-
53
- The item check skips `00-overview.md` and `99-next-session.md`, since neither holds items and running it over the handoff would report every heading it carries. The memory walk skips `index.md` for the same reason, since the catalog is generated from its siblings rather than authored as an entry.
54
-
55
- A memory `category` is compared against the sentence-case form of the filename prefix rather than checked field by field, so one finding covers a prefix outside the four types, a field disagreeing with the prefix, and a casing drift that would open a second group in the catalog. The body check runs on `feedback` and `project` entries alone, because a `user` or `reference` entry is a single sentence by design and has no rule to apply.
56
-
57
- The standards filename check derives a word from the governed path rather than counting words in the name. Every member of the corpus is named for the artifact its scope statement governs, and one word is what that produces rather than the rule itself, so a check keyed on word count would pass a conforming single word naming the wrong artifact. Each path segment offers its own word, a dotted container segment offers none, a hyphenated segment offers its parts, and the singular and the plural both match.
58
-
59
- The scope statement is read exactly as `scripts/standards/list.sh` reads it for the catalog's `appliesTo` field: backticked spans in the first sentence alone, with an attribute standard resolving where that sentence backticks nothing and the statement says it governs an attribute. One sentence read two ways would let a standard pass the check while publishing a different jurisdiction to every consumer of the catalog. An attribute standard is exempt from the filename derivation because it governs no path to derive from, and a statement that anchors nothing and claims no attribute is reported as `scope-unanchored` rather than passed, since silence there would let the backticks be removed to disable the check.
60
-
61
- An absent `## Scope` section suppresses the filename finding. The name derives from the statement, so a missing section leaves nothing to derive against, and reporting both would name one defect twice and point the fix at the wrong file. The walk skips `index.md`, which is generated from its siblings rather than authored.
62
-
63
- A plan section opens as a bold label or as an H2 and the check counts both, naming the standard's spelling when it reports one missing. The corpus splits roughly four to one between the two forms, so failing the variant would report nearly every plan on a rule that costs a reader nothing.
64
-
65
- A section runs to the next marker-shaped line whatever it names, so a plan carrying a label of its own closes the section above it rather than collecting into it. Fenced blocks are dropped before any of this, since a plan showing the shape it writes puts real-looking bullets and headings inside a fence.
66
-
67
- ### Exit codes and refusals
68
-
69
- Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so a record kind's refusal names both record roots and the `standards` refusal names the authoring root and the installed copy.
70
-
71
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `findings` array and its `reason` rather than the exit when a skill consumes this.
72
-
73
- The four record folders are shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. A linked worktree validates the same records every other session reads.
74
-
75
- `standards` takes the other default, the root of the checkout the caller stands in. The corpus is tracked, so a linked worktree holds its own edited copy, and resolving the main root there would report on a tree the session never touched while saying nothing about which one it read. A session that adds or renames a standard inside a worktree is the case, and it is the one the check exists for.
76
-
77
- Skills branch on the findings rather than on the exit code:
78
-
79
- ```bash
80
- canon records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
81
- ```
82
-
83
- For the shapes each check enforces, see `standards/plan.md`, `standards/groundwork.md`, `standards/intake.md`, `standards/memory.md`, and `standards/standard.md`.
20
+ `canon records validate <kind>` reports where a record folder or the standards corpus disagrees with the standard governing it. Its flags, the per-kind checks, and the refusals are in `records-validate.md`.
84
21
 
85
22
  ## Migrate
86
23
 
@@ -181,57 +118,4 @@ Exit codes: `0` nothing to prune, or `--write` deleted every candidate. `1` refu
181
118
 
182
119
  ## Push and pull
183
120
 
184
- `canon records push` commits the backed record folders to a private remote and pushes them. `canon records pull` fetches the other direction and writes them back. Both take `--json` and `--root` the way `validate` does, and both exit `0` on agreement and `1` on a refusal.
185
-
186
- ```bash
187
- canon records push
188
- canon records push --json
189
- canon records pull
190
- ```
191
-
192
- At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The report also lists every file new to the history under `added`, since a stray file inside a folder already tracked is visible only by name.
193
-
194
- The three excluded names bound the records index as well as the disk. A name an older binary committed, such as `tmp`, is removed from the history once and reported under `dropped`, and no later push stages it again. That removes it from the tip going forward and purges nothing from earlier commits the remote already holds. A project's own drafts folder needs no config to be backed: moved to `.canon/<name>/`, it is a top-level directory like any other and enters the payload on the next push. The legacy `.claude` root keeps a fixed allowlist instead, since that root also holds tracked `skills/`, `rules/`, and `hooks/` a push must never carry, and an exclusion set there would stage all three. Each name is a top-level record folder and every archive sits inside the one it archives, so the set stays at one entry per surface however many archives appear, and it deliberately does not match the six record kinds `validate` hardcodes.
195
-
196
- Records are gitignored by design, so the history lives in a second git directory at `.records.git` inside the record root, with that root as its work tree. Both resolve off the root together rather than folder by folder, since a history opened at one root beside a work tree at the other would stage the deletion of every folder a move relocated. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the backed folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Each pathspec is a bare folder name and git reads it against the current directory rather than against the work tree the same call names, so the invocation carries `-C` at the work tree beside the other two flags. That is what lets either verb run from a linked worktree under `.claude/worktrees/`, which sits inside the records work tree and would otherwise prefix every name with its own path.
197
-
198
- ### Setup
199
-
200
- A person creates the records repository once per machine, and the verbs refuse with the commands when it is absent:
201
-
202
- ```bash
203
- git --git-dir=.canon/.records.git init
204
- git --git-dir=.canon/.records.git remote add origin <private-repo-url>
205
- printf '.canon/\n' >> .gitignore
206
- ```
207
-
208
- The ignore line is repeated here rather than left to the install, because the person running these commands is the one who creates the directory and the rule is worth reading beside the command that needs it. The claude manifest ships `.canon/` too, so a project that ran `canon tooling sync` already carries it and this line is a no-op there. One root entry covers the history and every record beside it, which is what makes the rule worth stating once rather than per folder.
209
-
210
- Point it at a private repository, and at one that is not a remote of the project. Records carry the memory pen, the review reports, and the groundwork trails, so a public project publishes all of it to anyone who fetches all refs. `push` compares the configured origin against every remote of the project and refuses on a match. A read of that list which fails refuses as well, since an empty list clears the comparison for every origin and a gate that passes on its own failure is no gate.
211
-
212
- ### Refusals
213
-
214
- | Reason | What fired |
215
- | ------------------- | -------------------------------------------------------------------------------------- |
216
- | `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
217
- | `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
218
- | `no-remote` | The records history has no `origin` |
219
- | `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
220
- | `remote-shared` | The records origin is also a remote of the project |
221
- | `no-remote-records` | `pull` found no branch on the records origin |
222
- | `local-changes` | `pull` found records on disk that the history does not carry |
223
- | `local-ahead` | `pull` found local commits that never reached the origin |
224
- | `unsafe-payload` | `push` found a pending file over 25 MB or carrying a credential, named under `blocked` |
225
- | `git-failed` | A git call failed, with its stderr in the message |
226
-
227
- `split-roots` runs ahead of every gate below it and fires on a half-migrated tree, which is what a `canon migrate records` run that failed partway leaves. `recordRoot` answers for the whole tree on the first root that exists, so a folder left at the old root is absent from the work tree while the records index still names it, and an unguarded `add -A` would stage its deletion and drop it from the remote on the next push. Finish the move, or put the stranded folders back beside the others.
228
-
229
- `unsafe-payload` runs before anything is staged, so a refusal leaves the index, the log, and the object store as it found them. It reads every new or changed file the push would stage and blocks one over 25 MB, which is read off the file's size without opening it, or one carrying a value `canon secrets scan` would report, through the same patterns and the same `canon-allow-secret` marker. It refuses the whole push rather than skipping the file, and `--json` lists each path with its cause and a detail that names the credential's kind and line, never its value. Move the file out of the record folders, such as into `.canon/tmp/`, or remove the credential, then push again. The guard reads only what this push would add, so a credential committed before it existed stays in the history unreported.
230
-
231
- A push the remote rejects undoes the commit that run made and leaves the records on disk, so the next run does not send a payload the remote already refused. A history an older binary committed and left unpushed is outside that undo, and the guard never reads it either, so every push sends the same blob again. Reset it by hand from the project root to the last commit the origin holds, `git --git-dir=.canon/.records.git --work-tree=.canon reset <commit>`, and push again. The `--work-tree` is required, since the setup `init` marks the history bare and git refuses that reset on a bare repository.
232
-
233
- The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding no backed folders has nothing to lose, so a restore onto a fresh checkout runs straight through.
234
-
235
- ### When it runs
236
-
237
- `.husky/post-merge` runs `push` after the task archiving loop, on every merge rather than only on one that archived a task. A review report and a memory entry both land on runs that close nothing. The call sits inside an `if` and last in the file, so an unreachable remote neither aborts the hook nor delays the archiving above it, and a checkout that never ran the setup reports nothing. Anything the hook misses is covered by running the verb by hand.
121
+ `canon records push` backs the record folders to a private remote and `canon records pull` restores them. The payload, the setup, and the refusals are in `records-push.md`.
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: Sessions transfer
3
+ description: Moving a session to another machine, the export bundle and its manifest, placing it with import, the refusal reasons, and what a resume on the target depends on
4
+ ---
5
+
6
+ # Sessions transfer
7
+
8
+ The roster these verbs check an id against is in `sessions.md`.
9
+
10
+ ## Export
11
+
12
+ `canon sessions export <id>` packs one session into a single gzip tarball so it can resume on another machine. The bundle holds the transcript, its side folder with every path kept, and a manifest.
13
+
14
+ ```bash
15
+ canon sessions export <id>
16
+ canon sessions export <id> --out ~/session.tar.gz --json
17
+ ```
18
+
19
+ | Option | Behavior |
20
+ | -------------- | ---------------------------------------------------------- |
21
+ | `--out <path>` | Write the bundle here rather than under the scratch folder |
22
+ | `--json` | Add a machine-readable record on stdout |
23
+
24
+ Without `--out` the bundle lands at `.canon/tmp/session-export/<id>.tar.gz` under the main worktree root, which is gitignored and reclaimed by `canon records prune-tmp`. It writes locally and never uploads. A transcript holds whatever tool output passed through the session, credentials included where one was printed, so treat the bundle as private.
25
+
26
+ Exit codes: `0` the bundle was written, `1` refused. The refusal carries a `reason` of `not-found`, where no project folder holds a transcript for the id, or `no-archive`, where the running Bun predates `Bun.Archive`.
27
+
28
+ The lookup walks every folder under the configuration directory's `projects/`, so a transcript filed under any encoding is found. The transcript is read up to its last newline and never rewritten, so a session exporting itself mid-append drops at most the half line it was writing.
29
+
30
+ Export runs no skill. Write a handoff with `canon:session-compact` and push it with `canon records push` first where one is wanted. The manifest reports whether a handoff exists and whether it reached the records history, and whatever calls the verb owns that order.
31
+
32
+ ### The manifest
33
+
34
+ | Field | Holds |
35
+ | ------------------ | ---------------------------------------------------------------------------- |
36
+ | `format` | The bundle format version, `1` |
37
+ | `sessionId` | The exported id |
38
+ | `title` | The last custom title the transcript recorded, or `null` |
39
+ | `cwd` | The last working directory the transcript recorded, or `null` |
40
+ | `root` | The main worktree root of the repository the export read |
41
+ | `origin` | The `origin` remote reduced to `host/path`, or `null` |
42
+ | `branch`, `head` | The branch and commit of the checkout the export ran in |
43
+ | `recordsHead` | The records history `HEAD`, or `null` where no records history exists |
44
+ | `compacted` | Whether the transcript carries a compaction boundary |
45
+ | `live` | Whether the id was live in the roster at export |
46
+ | `handoff` | The newest `.canon/compact/` note written since the session began, or `null` |
47
+ | `handoffCommitted` | Whether that note is committed in the records history |
48
+ | `recommend` | `handoff` where a handoff exists, `transcript` otherwise |
49
+
50
+ `recommend` prefers the handoff even though the transcript resumes in full. A resumed transcript carries the source machine's absolute paths into a session working somewhere else, and a handoff states the work without them.
51
+
52
+ ## Import
53
+
54
+ `canon sessions import <bundle>` places an exported session under this machine's configuration directory so `claude` can resume it.
55
+
56
+ ```bash
57
+ canon sessions import session.tar.gz
58
+ canon sessions import session.tar.gz --root ~/repos/canon --json
59
+ ```
60
+
61
+ | Option | Behavior |
62
+ | --------------- | ---------------------------------------------------------- |
63
+ | `--root <path>` | The repository to resume in. Defaults to the main worktree |
64
+ | `--json` | Add a machine-readable record on stdout |
65
+
66
+ The transcript and side folder land under `projects/<encoding>/`, where the encoding is the root with every character outside `[A-Za-z0-9]` turned into a dash. Import writes new files only and never overwrites.
67
+
68
+ Exit codes: `0` the session was placed, `1` refused. The refusal carries one of these reasons:
69
+
70
+ - `not-a-bundle`: the file is no gzip tarball, carries no manifest at format `1`, or names a path outside its own session, such as one climbing out with `..` or an absolute one
71
+ - `other-repository`: the local `origin` differs from the one the bundle recorded
72
+ - `exists`: the target transcript or side folder is already there, or any other project folder already holds a transcript for the id, which `--resume <id>` could reach first
73
+ - `live-here`: the id is live in the local roster, so a second copy would fork it
74
+ - `write-failed`: a write failed partway for a reason other than a copy appearing, such as a bundle carrying a file where it also needs a folder
75
+ - `no-archive`: the running Bun predates `Bun.Archive`
76
+
77
+ A write that fails partway removes every file this run placed and every folder it created before refusing, so a retry of the same bundle does not refuse on its own leftovers. Any path it could not remove is named in the message. A copy that appears between the existence check and the write refuses as `exists` on the same terms.
78
+
79
+ A placed session can still lag the source. The record's `behind` list carries `repository-behind` when this checkout lacks the exported `HEAD`, and `records-behind` when the records history lacks the exported records commit, which `canon records pull` brings. Neither refuses, since each has a remedy the reader runs before resuming.
80
+
81
+ On success the record carries two `routes`. One runs `claude --resume <id>` from the root, and the other starts `claude` there and runs `/canon:session-resume` bare against the handoff.
82
+
83
+ ### What resume depends on
84
+
85
+ Measured on `claude` 2.1.281 on 2026-09-24, `--resume <id>` found a copied transcript in any folder under `projects/` and chained the next turn onto its full history, run from a repository path the transcript never recorded. `--continue` found it only under the local encoding, which is why import derives the folder rather than keeping the source machine's.
86
+
87
+ The encoding is inferred from observed folders rather than read from a published rule, and how a client shortens a path past its length limit was never observed. A client changing the rule leaves `--continue` looking elsewhere while `--resume <id>` still works. The framed report names the derived folder so a reader can compare.
88
+
89
+ The side folder did not change the conversation sent. What it carries is `tool-results/`, which a `persisted-output` pointer in the transcript names by the source machine's absolute path, and `subagents/`, which holds background agent transcripts. On a machine whose path matches, those pointers resolve. On one whose path differs, they dangle.
90
+
91
+ Resuming on the target while the source session keeps running forks the conversation, and nothing detects that across machines. The manifest's `live` field and the printed routes are the only warning.