@erclx/canon 4.8.0 → 4.8.1

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/README.md CHANGED
@@ -4,11 +4,23 @@
4
4
  [![CI status](https://img.shields.io/github/actions/workflow/status/erclx/canon/verify.yml?branch=main)](https://github.com/erclx/canon/actions/workflows/verify.yml)
5
5
  [![License MIT](https://img.shields.io/npm/l/@erclx/canon)](LICENSE)
6
6
 
7
- One source for your repos' AI conventions. Install once, sync everywhere.
7
+ canon is a CLI and Claude Code plugin that stops your AI conventions drifting apart across repositories. It keeps one authoritative copy and installs it into each project on demand.
8
8
 
9
9
  ![The canon catalog, listing skills, governance rules, and standards with the count each ships, the workflow skills named, and a sample of the rule and standard names](assets/hero.png)
10
10
 
11
- If you work across more than one repository and your AI setup has started to drift between them, this is for you. The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
11
+ The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
12
+
13
+ ## Why
14
+
15
+ If you work across more than one repository and your AI setup has started to drift between them, this is for you. Every AI coding setup accumulates the same assets. Prompts to reuse, rules agents should follow, slash commands, skills, seed docs, sync scripts. Once you have enough projects, your copies drift and your agents stop getting consistent signals.
16
+
17
+ Three design choices shape the toolkit.
18
+
19
+ - Agent-first: every command has a non-interactive path and a JSON catalog. If a Claude Code skill or any other agent cannot drive the CLI without prompts, the design is wrong.
20
+ - Text-native: conventions, rules, and prompts are authored as markdown that you and your agents read the same way. No hidden behavior, no compiled state.
21
+ - One source, many consumers: this repo is the authoritative copy. Your projects install and sync on demand, never author in place.
22
+
23
+ Two limits worth knowing before you install. Claude Code is the only agent runtime the plugin targets, and the CLI needs Bun on your path.
12
24
 
13
25
  ## Install
14
26
 
@@ -31,6 +43,31 @@ bun install --global @erclx/canon
31
43
 
32
44
  [Bun](https://bun.sh) is the CLI runtime and has to be on your path first. Confirm the install by resolving `canon --help`.
33
45
 
46
+ ## What is inside
47
+
48
+ Each domain has a canonical source in this repo and a thin install or sync CLI on your side.
49
+
50
+ - Claude Code plugin: skills that plan a feature, review a diff, sync the planning docs, and run the ship chain from branch through pull request
51
+ - Governance rules: coding and authoring rules that load into a Claude session when a matching path is edited, installed per project and refreshed by sync
52
+ - Standards: authoring conventions for commits, branches, plans, tasks, and markdown, read by name with `canon standards <name>` rather than copied into your project
53
+ - Snippets: reusable prompts fired by `@` reference in a Claude Code session, resolved live from the plugin with no install step
54
+ - Tooling stacks: golden configs, seeds, and a reference per framework, laid down by `canon init` and reconciled by `canon tooling sync`
55
+ - Design system: a `DESIGN.md` token format, a skill that drafts one from an existing project or from scratch, and a render command
56
+ - Slides: a `SLIDES.md` source format with a layout catalog, rendered to PowerPoint by `canon slides render`
57
+ - Transcripts: `canon transcripts` pulls a YouTube transcript with metadata frontmatter into any repo
58
+ - Sandbox: scenario-based scaffolds that provision an isolated project state for verifying each domain flow
59
+
60
+ ## Documentation
61
+
62
+ Scaffolding your first project? Start with target projects, then the AI workflow loop. Everything else answers questions that arrive later.
63
+
64
+ - [AI workflow](docs/ai-workflow.md): feature-development loop inside a toolkit-managed project
65
+ - [Operating model](docs/operating-model.md): orchestrator, planner, and worker roles for building across parallel sessions
66
+ - [Visual design workflow](docs/visual-design-workflow.md): tiered guide for design and wireframe authoring
67
+ - [Target projects](docs/target-projects.md): scaffold, add a domain later, sync upstream drift
68
+ - [Agents](docs/agents/index.md): CLI flags, exit codes, and JSON output shapes
69
+ - [Docs index](docs/index.md): every reference doc in this repo
70
+
34
71
  ## Update
35
72
 
36
73
  Nothing refreshes on its own. Claude Code ships auto-update off for third-party marketplaces, so an installed copy serves whatever version it was installed at until you refresh it.
@@ -49,43 +86,6 @@ You don't have to wait until something breaks to find out you're behind. `canon
49
86
 
50
87
  To stop doing this by hand, turn auto-update on once under `/plugin` in the Marketplaces tab. Confirm what you are running with `canon --version` and `claude plugin list`.
51
88
 
52
- ## Why
53
-
54
- Every AI coding setup accumulates the same assets. Prompts to reuse, rules agents should follow, slash commands, skills, seed docs, sync scripts. Once you have enough projects, your copies drift and your agents stop getting consistent signals.
55
-
56
- Three design choices shape the toolkit.
57
-
58
- - Agent-first: every command has a non-interactive path and a JSON catalog. If a Claude Code skill or any other agent cannot drive the CLI without prompts, the design is wrong.
59
- - Text-native: conventions, rules, and prompts are authored as markdown that you and your agents read the same way. No hidden behavior, no compiled state.
60
- - One source, many consumers: this repo is the authoritative copy. Your projects install and sync on demand, never author in place.
61
-
62
- Two limits worth knowing before you install. Claude Code is the only agent runtime the plugin targets, and the CLI needs Bun on your path. Snippets are the one surface that also travels to Gemini chat.
63
-
64
- ## What is inside
65
-
66
- Each domain has a canonical source in this repo and a thin install or sync CLI on your side. The links run to internal narrative, written for someone maintaining the toolkit rather than installing it, so skip them on a first pass.
67
-
68
- - [Claude Code plugin](.claude/context/claude-plugin/index.md): skills for planning, review, docs sync, and the git ship chain
69
- - [Governance rules](.claude/context/governance/index.md): Claude rules and stacks, installable per project
70
- - [Standards](.claude/context/standards/index.md): shared authoring conventions, read by name rather than installed
71
- - [Snippets](.claude/context/snippets.md): reusable prompts for Claude and Gemini chat
72
- - [Tooling stacks](.claude/context/tooling.md): golden configs, seeds, and references per framework
73
- - [Design system](.claude/context/design.md): `DESIGN.md` token shape, extract skill and its two paths, render command
74
- - [Slides](.claude/context/slides.md): `SLIDES.md` source, layout catalog, render command, draft skill
75
- - [Transcripts](.claude/context/transcripts.md): fetch a YouTube transcript with metadata frontmatter via `canon transcripts`
76
- - [Sandbox](.claude/context/sandbox/index.md): scenario-based scaffolds for verifying each domain flow
77
-
78
- ## Documentation
79
-
80
- Scaffolding your first project? Start with target projects, then the AI workflow loop. Everything else answers questions that arrive later.
81
-
82
- - [AI workflow](docs/ai-workflow.md): feature-development loop inside a toolkit-managed project
83
- - [Operating model](docs/operating-model.md): orchestrator, planner, and worker roles for building across parallel sessions
84
- - [Visual design workflow](docs/visual-design-workflow.md): tiered guide for design and wireframe authoring
85
- - [Target projects](docs/target-projects.md): scaffold, add a domain later, sync upstream drift
86
- - [Agents](docs/agents/index.md): CLI flags, exit codes, and JSON output shapes
87
- - [Docs index](docs/index.md): every reference doc in this repo
88
-
89
89
  ## Development
90
90
 
91
91
  Working on the toolkit starts from a clone. Running the CLI doesn't, since it installs from the registry. Skip this section unless you're changing the toolkit itself.
@@ -97,7 +97,7 @@ Working on the toolkit starts from a clone. Running the CLI doesn't, since it in
97
97
  - [GitHub CLI](https://cli.github.com) (optional) for ship flows
98
98
  - Shell: `zsh` or bash 4+ (`brew install bash` on macOS).
99
99
 
100
- Clone the repo, then run the bootstrap script. It installs dependencies, links the CLI globally, and adds the Claude Code shell aliases to your `~/.zshrc`.
100
+ Clone the repo, then run the bootstrap script. It installs dependencies, links the CLI globally, and appends a marked block of Claude Code shell aliases to your `~/.zshrc`.
101
101
 
102
102
  ```bash
103
103
  git clone https://github.com/erclx/canon.git
@@ -106,7 +106,7 @@ bun install
106
106
  bun run bootstrap
107
107
  ```
108
108
 
109
- The script is idempotent, so re-run it after pulling upstream changes without duplicating anything. It confirms the install by resolving `canon --help` on the last step. See [zshrc aliases](docs/zshrc-aliases.md) for what each alias does.
109
+ The script is idempotent, so re-run it after pulling upstream changes without duplicating anything. That also means it leaves an alias block you already have alone rather than refreshing it, so an alias added upstream needs the block deleted and the script re-run. It confirms the install by resolving `canon --help` on the last step. See [zshrc aliases](docs/zshrc-aliases.md) for what each alias does, how to pick up a new one, and how to opt out of the block.
110
110
 
111
111
  With the CLI linked, scaffold a fresh project.
112
112
 
@@ -120,6 +120,20 @@ canon init
120
120
 
121
121
  For the full journey from scaffold through adding a domain later to syncing upstream drift, see [target projects](docs/target-projects.md).
122
122
 
123
+ ### Internal narrative
124
+
125
+ Each domain carries an entry written for someone maintaining the toolkit rather than installing it. These paths resolve in a clone only. The published package ships `docs` and not `.claude`, so an installed copy does not carry them.
126
+
127
+ - [Claude Code plugin](.claude/context/claude-plugin/index.md)
128
+ - [Governance rules](.claude/context/governance/index.md)
129
+ - [Standards](.claude/context/standards/index.md)
130
+ - [Snippets](.claude/context/snippets.md)
131
+ - [Tooling stacks](.claude/context/tooling.md)
132
+ - [Design system](.claude/context/design.md)
133
+ - [Slides](.claude/context/slides.md)
134
+ - [Transcripts](.claude/context/transcripts.md)
135
+ - [Sandbox](.claude/context/sandbox/index.md)
136
+
123
137
  ## Contributing
124
138
 
125
139
  Portfolio project. Issues are welcome. Pull requests are accepted by invitation only, so open an issue rather than a branch. Read the [contributing guidelines](CONTRIBUTING.md) for the local loop, the authoring split, and the commit convention before you file anything.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.8.0",
4
+ "version": "4.8.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -38,10 +38,10 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
38
38
  ## Must not
39
39
 
40
40
  - Infer a new task from the diff. Only outcomes already on the board get marked.
41
- - Touch task files the session did not change, outside the board-wide plans sweep that exists to clear a missed archive
41
+ - Touch task files the session did not change
42
42
  - Widen what a writing step reads when the baseline is unusable. Widening a read is safe and widening a write stubs a surface for every file in the repository.
43
43
  - Edit `CLAUDE.md` inline. Every change there goes through a diff-and-approve gate, so this skill only flags.
44
- - Create a context entry or delete a plan
44
+ - Create a context entry, or move or delete a plan. A plan is settled by the merge, and `canon tasks archive` carries it.
45
45
  - Overwrite a file a promotion block routes to. A destination that already holds a page is a merge for a person, and folding over it discards work this skill never read.
46
46
  - Write an anchor onto a decision the run did not amend, or refresh one without re-reading the number. A date from a pass that measured nothing is the false confidence the marker exists to prevent.
47
47
  - Anchor an entry written before the rule, which dates it by blame rather than by a read
@@ -51,7 +51,7 @@ Read these in parallel from the current worktree root (`pwd`), not the main work
51
51
 
52
52
  Read the task board from the main worktree root instead, per Worktrees in `CLAUDE.md`. It is gitignored scratch and never commits with the branch:
53
53
 
54
- - `.canon/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step. The scratch sweep reads every file in the folder for its plans sweep and states that where it gives the instruction.
54
+ - `.canon/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step, which is the only step here that opens a task file.
55
55
 
56
56
  ## Step 2: identify what changed
57
57
 
@@ -102,7 +102,7 @@ For each doc with relevant changes, apply updates following these rules. Read a
102
102
 
103
103
  The verb resolves the board at the main worktree root in-process, which is the route because this is an edit inside an existing file and the file-editing tools refuse that path from a linked worktree.
104
104
 
105
- Read `ok` and `reason` out of that record rather than the exit, for the reason the plans sweep below states at length. A refusal arriving as success leaves the outcome unmarked while the chain moves on, so the board reports shipped work as open and the next session re-plans it.
105
+ Read `ok` and `reason` out of that record rather than the exit. An operator's shell profile may wrap `canon` in a function that runs the binary and then a second command and takes the second status, which flattens every non-zero exit to zero. A refusal arriving as success leaves the outcome unmarked while the chain moves on, so the board reports shipped work as open and the next session re-plans it.
106
106
 
107
107
  **REQUIREMENTS.md, ARCHITECTURE.md, DESIGN.md, `.claude/wireframes/<surface>.md`**
108
108
 
@@ -208,40 +208,13 @@ Report a block left unfolded rather than dropping it:
208
208
 
209
209
  `⚠ Skipped: <destination path> already exists. Merge by hand.`
210
210
 
211
- ## Step 9: sweep consumed scratch
211
+ ## Step 9: sweep consumed receipts
212
212
 
213
- Sweep reviews this session consumed, and sweep plans across the whole board. Resolve all paths at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
213
+ Sweep the review and memory receipts this session consumed. Resolve all paths at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
214
214
 
215
- Every move and delete below is a shell operation, so send each as a plain single `Bash` command rather than joining a `mkdir -p` to the `mv` with `&&`, which is refused as compound from a linked worktree. The one edit inside an existing file is the `Plan:` retarget, and no verb covers it: read the task file and write it back whole with a heredoc, which the file-editing tools refuse from a linked worktree and no shell stream editor may do.
215
+ Every delete below is a shell operation, so send each as a plain single `Bash` command rather than joining two with `&&`, which is refused as compound from a linked worktree.
216
216
 
217
- ### Plans
218
-
219
- Scan every file in `.canon/tasks/`, not only the ones this session touched. For each task file whose outcomes are now all `[x]`, check for a `Plan:` line directly under the title and parse the target.
220
-
221
- The line carries a markdown link, so read the target out of the parentheses rather than taking the rest of the line. A task still carrying the older bare-path form parses the same way once the link is absent, so accept both. Resolve the target against `.canon/tasks/` before routing on it, which lands `../plans/x.md` and `.canon/plans/x.md` on the same file.
222
-
223
- The bullets below name resolved locations, so an unresolved target falls to the last one and no plan is ever archived. Never delete a plan. `${CLAUDE_SKILL_DIR}/../../standards/plan.md` owns the archive destination and why a shipped plan is moved rather than removed.
224
-
225
- Board-wide scope is the one place this sweep reaches past Step 3's rule against touching task files the session did not change. A board carrying a task that closed while an earlier run missed its archive is the defect this exists to clear, and skipping those tasks would preserve it. Reaching them is safe because the archive moves the plan and points the task at the new path, so a task from unrelated work ends up with a working pointer rather than a broken one.
226
-
227
- Before moving anything, count the other citations. Scan every `.canon/tasks/*.md` file except the one being processed for a `Plan:` line naming the same plan. Compare the resolved target from the parse above, never the raw target string and never the filename alone.
228
-
229
- A board carrying one task written `../plans/x.md` and another written `.canon/plans/x.md` cites one plan, and a raw string comparison reads two, counts zero, and archives the file out from under a live task. Comparing filenames swaps that for the opposite error, since a live plan and an archived one share a basename whenever a closed task still points into `.canon/plans/archive/`, and the count then reads a citation that does not exist and archives nothing.
230
-
231
- Exclude the closing task explicitly. It sits on the board and cites the plan itself, so a scan that counts it never reaches zero and no plan is ever archived.
232
-
233
- `canon tasks plan-citations <stem> --json` answers this same question, and the archive gate already reads it. This body states the rule anyway rather than calling the verb, because a plugin skill reaches a target the moment it merges while the CLI reaches one only when a release publishes, so a target whose installed `canon` predates the verb gets no record back and routes on nothing. Measured against the `claude:docs` `board-sweep` arm, where calling the verb archived neither plan and created no `.canon/plans/archive/`.
234
-
235
- Nothing in the exit code reports that. Branch on the record's `ok` and `reason` fields and never on the exit, which is the rule every task verb already carries: an operator's shell profile may wrap `canon` in a function that runs the binary and then another command, taking its status from the second, and one measured here masks every non-zero exit rather than only an absent verb. The binary itself exits 1 for an unknown subcommand and 1 for an ordinary refusal alike. Switching this body to the verb needs a release that carries it and a read of the record rather than the exit, which together retire the duplication.
236
-
237
- A plan can serve more than one task, and archiving on the first task to close strands every other task's pointer at a path that has moved. `.canon/plans/` is gitignored, so that retarget would be the only record and there is nothing to recover it from.
238
-
239
- - Target resolves inside `.canon/plans/`, the file exists, and no other task file cites it: create `.canon/plans/archive/`, move the file there under its original name, overwriting any file already sitting at that name. Then rewrite the task file's `Plan:` line to the archive path, so a completed task still leads to the reasoning behind it.
240
- - Target resolves inside `.canon/plans/` and at least one other task file cites it: leave the plan where it is and retarget nothing. Report the shared citation.
241
- - Target resolves inside `.canon/plans/archive/`: skip silently. The plan was archived by an earlier pass and the task file is already correct.
242
- - Any other resolved target outside `.canon/plans/`: warn and skip.
243
-
244
- Write the retarget as a markdown link, `Plan: [feature-<slug>](../plans/archive/feature-<slug>.md)`, updating both halves so the text and the target stay in step. This branch is the only writer that produces a `Plan:` line nobody authored by hand, so a retarget that emits a bare path converts every task to the old form as it closes and drifts the board back to two shapes on its own.
217
+ Plans are not swept here. A plan is settled by the merge rather than by an outcome this run marked, and `canon tasks archive` moves it with the task the `post-merge` hook archives. Sweeping it from this step read a closure Step 3 had written moments earlier and moved a plan the branch was still building from.
245
218
 
246
219
  ### Reviews
247
220
 
@@ -253,7 +226,7 @@ Sweep the branch reports this session never opened. List `.canon/review/branch/r
253
226
 
254
227
  What that removes is a local-only review on a branch deleted before it opened a pull request. `claude-review` says so where a reader meets the report, and the sweep runs anyway rather than keeping every report against the one case, since nothing else ever clears them.
255
228
 
256
- Memory receipts sweep board-wide, like both halves of this step above them. Scan every `.canon/review/memory/memory-review-*.md`, not only the one matching this slug. `claude-memory-review` writes its receipt after this skill has run in every ship chain, so a sweep keyed on the current slug looks for a file that does not exist yet, and no later branch looks for it either because a slug is unique per feature. Scanning the folder is what makes the sweep fire at all.
229
+ Memory receipts sweep board-wide rather than by slug. Scan every `.canon/review/memory/memory-review-*.md`, not only the one matching this slug. `claude-memory-review` writes its receipt after this skill has run in every ship chain, so a sweep keyed on the current slug looks for a file that does not exist yet, and no later branch looks for it either because a slug is unique per feature. Scanning the folder is what makes the sweep fire at all.
257
230
 
258
231
  For each receipt, count the H2 items still marked 📝 pending:
259
232
 
@@ -266,8 +239,6 @@ Do not sweep `ui-checklist-*.md` (pending human verification), `ux-audit-*.md`,
266
239
 
267
240
  Output one line per file swept:
268
241
 
269
- - `📦 Archived: <path>` for a plan moved into `.canon/plans/archive/`
270
- - `⏭ Kept: <path>, still cited by <task-file>` for a plan another live task shares
271
242
  - `🧹 Deleted: <path>, branch gone` for a branch report whose branch no longer exists
272
243
  - `🧹 Deleted: <path>, folded <n> skips` for a swept memory receipt
273
244
  - `⏭ Kept: <path>, <n> items pending` for a memory receipt still holding decisions
@@ -73,13 +73,17 @@ Name `<model>` on the launch, and pick it against the task rather than copying w
73
73
  ## Dispatch
74
74
 
75
75
  ```bash
76
- claude --bg --model <model> -n "worker-<slug>" "Run /canon:claude-worktree <type>/<slug>, then /canon:claude-autoship <plan>. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the pull request opens, carrying the number, the branch, the head sha, the CI state, and every point you departed from the plan on, and message it again if you stop on a question."
76
+ claude --bg --model <model> -n "worker-<project>-<slug>" "Run /canon:claude-worktree <type>/<slug>, then /canon:claude-autoship <plan>. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the pull request opens, carrying the number, the branch, the head sha, the CI state, and every point you departed from the plan on, and message it again if you stop on a question."
77
77
  ```
78
78
 
79
79
  `--bg, --background` starts the session as a background agent and returns immediately, `-n, --name` sets the display name that tells a self-dispatched worker from an operator's own launch in `canon sessions list`, and `--model` overrides the inheritance the section above measured. Pass `-n` on every dispatch rather than letting the client derive one. A launch that omits it leaves the session named for a fragment of its own identifier, which is both its address on the send channel and the whole of what the operator sees for it in agent view.
80
80
 
81
81
  The prefix reads `worker-` because that is the role it marks. It read `orchestrator-` until 2026-08-31, and no controlling session ever carried it, so a worker filtering the roster for that string found a sibling or itself on every row. Nothing matches the prefix programmatically, which is what kept the rename down to three strings.
82
82
 
83
+ `<project>` is the basename of the main worktree root, not of wherever the dispatcher happens to be running. Resolve the main root first, the way `claude-worktree` Step 1 does, since a bare `git rev-parse --show-toplevel` inside a linked worktree returns the worktree path rather than the project's. See Worktrees in `CLAUDE.md`.
84
+
85
+ `claude agents` lists every session on the machine with no path column and no per-project filter, so `<project>` in the name is the only thing left telling two fleets apart, and a session named off the worktree path instead would carry the branch folder rather than the project. Two projects each dispatching a bare `worker-page-driver` used to read as one row in that view.
86
+
83
87
  Read `<dispatcher-id>` with `canon sessions list --self --json` and interpolate the `sessionId` that row carries. Carry the id rather than the name. A name is derived from whatever the session turned out to be doing, and across the 181 records stamping both fields, nine were renamed after launch at a median of 5.4 minutes and a maximum of 509. Three landed more than ten minutes in, which is inside the window a worker announces its pull request in, so a name written into the prompt is aimed at a send that happens after it goes stale.
84
88
 
85
89
  Where the installed CLI answers `--self` with an unknown option, that flag is newer than the release the target holds. Read the `sessionId` from the record the client writes for this session under its configuration directory, and say which route answered so the reader knows whether the id was read or inferred.
@@ -165,11 +169,11 @@ directly with `Bash`, `Read`, and `Edit` instead of retrying the tool, which
165
169
  is the route two workers already took today on two different branches.
166
170
 
167
171
  ```bash
168
- claude --bg --model <model> -n "worker-<slug>" "Enter the worktree for <branch> at .claude/worktrees/<slug>/, creating it from that branch if the folder is gone. Run /canon:claude-worker, then /canon:claude-address-review. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the address pass finishes, carrying what was addressed and the PR's CI state, and message it again if you stop on a question."
172
+ claude --bg --model <model> -n "worker-<project>-<slug>" "Enter the worktree for <branch> at .claude/worktrees/<slug>/, creating it from that branch if the folder is gone. Run /canon:claude-worker, then /canon:claude-address-review. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the address pass finishes, carrying what was addressed and the PR's CI state, and message it again if you stop on a question."
169
173
  ```
170
174
 
171
- `<dispatcher-id>` and `<model>` resolve the same way the build shape resolves
172
- them above.
175
+ `<dispatcher-id>`, `<model>`, and `<project>` resolve the same way the build
176
+ shape resolves them above.
173
177
 
174
178
  Take this shape wherever a review needs answering and no live session already
175
179
  holds the branch. Where one does, message it to run `claude-address-review`
@@ -196,14 +200,15 @@ gitignored file at the main worktree root, so this shape names the row's task
196
200
  file rather than a branch and opens with the role instead of a worktree call.
197
201
 
198
202
  ```bash
199
- claude --bg --model <model> -n "planner-<slug>" "Run /canon:claude-planner, then /canon:claude-feature <task>. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the plan lands, carrying the path and what the task file got wrong, and message it again if you stop on a question."
203
+ claude --bg --model <model> -n "planner-<project>-<slug>" "Run /canon:claude-planner, then /canon:claude-feature <task>. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the plan lands, carrying the path and what the task file got wrong, and message it again if you stop on a question."
200
204
  ```
201
205
 
202
206
  `<task>` is the row's task file path and `<slug>` the slug its plan will take,
203
207
  resolved off the row the way the build shape resolves one off a plan.
204
- `<dispatcher-id>` and `<model>` resolve the same way they do above. The prefix
205
- reads `planner-` for the reason the worker's reads `worker-`, which is that it
206
- marks the role of the session it names rather than the one that launched it.
208
+ `<dispatcher-id>`, `<model>`, and `<project>` resolve the same way they do
209
+ above. The prefix reads `planner-` for the reason the worker's reads `worker-`,
210
+ which is that it marks the role of the session it names rather than the one
211
+ that launched it.
207
212
 
208
213
  None of the three checks above binds this shape. The branch check has no
209
214
  candidate to read, and the disjointness gate has nothing to compare, since a
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: claude-tasks
3
- description: Creates a task file in `.canon/tasks/` with the filename, phase label, and frontmatter the standard requires, and archives a shipped one out of the folder. Use when asked to "add a task", "create a task", "queue this", "put this on the board", "archive that task", or "close out a shipped task". Do NOT use to mark an outcome `[x]` or to archive a plan. That is `claude-docs`.
3
+ description: Creates a task file in `.canon/tasks/` with the filename, phase label, and frontmatter the standard requires, and archives a shipped one out of the folder. Use when asked to "add a task", "create a task", "queue this", "put this on the board", "archive that task", or "close out a shipped task". Do NOT use to mark an outcome `[x]`. That is `claude-docs`.
4
4
  ---
5
5
 
6
6
  # Claude tasks
7
7
 
8
- Owns the two operations that bring a task file into existence and take it out of the folder. `claude-docs` edits the contents of a task that already exists, marking outcomes `[x]` and sweeping the plans those tasks cite. Do not mark outcomes here and do not archive a plan on its own.
8
+ Owns the two operations that bring a task file into existence and take it out of the folder. `claude-docs` edits the contents of a task that already exists, marking outcomes `[x]`. Do not mark outcomes here, and do not move a plan by hand: the archive carries it.
9
9
 
10
10
  Read `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` before writing any file. It holds the filename convention, the frontmatter contract, and the file format. Do not work them from memory.
11
11
 
@@ -104,19 +104,18 @@ canon tasks archive <stem> --json
104
104
 
105
105
  The command refuses rather than reports, and the refusal reaches this skill through the record rather than through the exit. Branch on `ok`, then on `reason`. An operator's shell profile may wrap `canon` in a function that runs the binary and then a second command and takes the second status, which masks every non-zero exit rather than only an absent verb. The binary exits 1 for an unknown subcommand and 1 for an ordinary refusal alike, so the record is the only signal that survives the wrapper.
106
106
 
107
- On success the record carries `from`, `to`, `priorityRowRemoved`, and `indexRegenerated`, which is what moved, what row it cleared, and whether the index changed.
107
+ On success the record carries `from`, `to`, `priorityRowRemoved`, and `indexRegenerated`, which is what moved, what row it cleared, and whether the index changed. It also carries `plan` when the task was the last live citation of a live plan, holding the `from` and `to` of the plan moved alongside it. A `plan` of `null` means the task cited no live plan, or that a sibling still holds it.
108
108
 
109
109
  ### Step 3: route on a refusal
110
110
 
111
111
  Each reason has one resolution and none of them is to archive around it:
112
112
 
113
113
  - `open-outcomes`: the named outcomes are unmarked or genuinely open. Run `claude-docs` when the work shipped and nothing marked it. Leave the task on the board when the outcome is real. Cut the outcomes first when the work is being abandoned, so the board records what was dropped.
114
- - `plan-unswept`: stop and route to `claude-docs`, which owns the plans sweep and the last-live-citation rule. `❌ Plan not yet swept. Run /claude-docs first, then archive.`
115
114
  - `ambiguous`: two tasks name one pull request, which is the misfile `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` rules out. Resolve the citation by hand, since no sweep repairs it.
116
115
  - `no-match`: the stem or number names nothing on the board. Check the name against the listed stems.
117
116
  - `bad-input`: the command line was wrong rather than the board. Read the message, fix the arguments, and run it again. Nothing on the board needs repair, which is what separates this from the two above.
118
117
 
119
- Do not move a plan from this skill. `claude-docs` owns that move. Two skills relocating the same file drift into relocating it differently.
118
+ Do not move a plan by hand from this skill. The command carries the plan with the task when no other live task cites it, and retargets the archived task's `Plan:` line at the new path. A second mover drifts into relocating the same file differently.
120
119
 
121
120
  Leave `TASK-ARCHIVE.md` alone when it is present in the archive folder. It records the single-file era in the shape that era used, and splitting it would fabricate per-task files nobody wrote.
122
121
 
@@ -72,8 +72,8 @@ Infer only where no operator is present. Read `canon sessions list --json` and
72
72
  take the sessions holding no feature branch as the candidates, since a
73
73
  controlling session holds none. Say the addressee was inferred so the reader can
74
74
  correct it. Never filter that roster by name prefix: every self-dispatched
75
- worker is named `worker-<slug>`, so a prefix scan returns a sibling or this
76
- session itself, which is the defect that sent messages owed to a controller
75
+ worker is named `worker-<project>-<slug>`, so a prefix scan returns a sibling or
76
+ this session itself, which is the defect that sent messages owed to a controller
77
77
  somewhere else.
78
78
 
79
79
  Report a resolution that returns nothing rather than falling back to a guess.
@@ -26,7 +26,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
26
26
  | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
27
27
  | `canon feedback` | Write toolkit feedback from stdin to `.canon/review/feedback/`, or open a GitHub issue with `--github` |
28
28
  | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
29
- | `canon tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
29
+ | `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
30
30
  | `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
31
31
  | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
32
32
  | `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
@@ -5,9 +5,9 @@ description: What this folder covers, the invocation rules every command inherit
5
5
 
6
6
  # Overview
7
7
 
8
- CLI catalog and invocation rules for agents working in this repository.
8
+ CLI catalog and invocation rules for an agent driving the `canon` CLI, in this repository or in any project that installed it.
9
9
 
10
- This folder is an index of what an agent can run and how to run it cleanly from a script. It does not cover domain behavior. Read `CLAUDE.md` for project behaviors and load the matching `.claude/skills/internal-*` skill when working inside a domain.
10
+ This folder is an index of what an agent can run and how to run it cleanly from a script. It does not cover domain behavior. Read the project's own `CLAUDE.md` for that. The `.claude/skills/internal-*` skills carry the per-domain editing guidance and live in the toolkit checkout alone, so a reader who installed the package resolves none of them.
11
11
 
12
12
  ## Invocation rules
13
13
 
@@ -22,5 +22,5 @@ See `CLAUDE.md` design principles. They apply to every command in this folder.
22
22
  ## Related
23
23
 
24
24
  - `CLAUDE.md`: project behaviors and design principles
25
- - `.claude/skills/internal-*`: domain-scoped guidance for editing work
25
+ - `.claude/skills/internal-*`: domain-scoped guidance for editing work, in the toolkit checkout only
26
26
  - `docs/index.md`: full docs directory
@@ -22,9 +22,13 @@ canon tasks archive --pull-request 673 --json
22
22
  | `--json` | Emit a machine-readable record on stdout |
23
23
  | `--root <path>` | Board root, defaulting to the main worktree |
24
24
 
25
- Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, `plan-unswept`, or `bad-input`.
25
+ Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, or `bad-input`.
26
26
 
27
- `plan-unswept` fires on the last task pointing at a live plan, never on every task pointing at one. The gate counts the other live tasks whose `Plan:` line resolves onto the same file, so a plan several tasks share archives its tasks freely and only the final one is held until `claude-docs` sweeps the plan. Reading the folder alone refused all of them, which deadlocked the board against a sweep correctly declining to move a plan another live task cites.
27
+ The task carries its plan with it. When the closing task is the last live one whose `Plan:` line resolves onto that file, the plan moves to `.canon/plans/archive/` under its own name and the archived task's line is rewritten as `Plan: [feature-<slug>](../../plans/archive/feature-<slug>.md)`, a folder deeper than the live task wrote it. The `plan` field on the success record carries that `from` and `to`, and is `null` when nothing moved.
28
+
29
+ A plan several tasks share stays where it is, and the task archives anyway. Moving it on the first task to close strands every sibling's pointer at a path that has gone, and `.canon/plans/` is gitignored so no history recovers the target. A `Plan:` line resolving to no file leaves the plan alone too, since a pointer somebody typed wrong is not a plan to move and holding the whole archive over it would park the board behind a repair the merge cannot make.
30
+
31
+ The merge is what settles a plan. `.husky/post-merge` reads the pull request number off the squash subject and calls this verb, so both halves close in one act with nobody naming a file. That is why the move sits inside this verb rather than in a second call the hook would make after it, which could leave the task archived and the plan live.
28
32
 
29
33
  `bad-input` covers a malformed command line, which all three task verbs answer the same way. It is separate from `ambiguous` and `no-match` because those describe the board, and a caller that passed two selectors would otherwise be sent to repair a task citation that is fine.
30
34
 
@@ -53,7 +57,7 @@ The record carries `location`, one of `unstated`, `live`, `archived`, or `outsid
53
57
 
54
58
  The target resolves against the board folder and against the project root both, so `../plans/x.md` and `.canon/plans/x.md` land on the same file and one plan two tasks spelled differently counts once. Containment is tested at both record roots rather than at the one this tree resolves at, since a line somebody wrote against a root the tree has since left is still a path into the plans folder, and reading it as outside would report a shipped plan as still live. `docs/agents/records.md` states the read order.
55
59
 
56
- `canon tasks archive` gates on this same answer, so a caller wanting the count reads it here rather than scanning the board. The `claude-docs` plans sweep is the exception and still states the rule in its own body, because a plugin skill reaches a target on merge while the CLI reaches one on release, so a sweep calling a verb the installed `canon` predates gets no record back and archives nothing.
60
+ `canon tasks archive` decides its plan move on this same answer, so a caller wanting the count reads it here rather than scanning the board.
57
61
 
58
62
  Branch on `reason` rather than on the exit code, which is the rule the archive section above already states and which this verb needs for a second reason. An operator's shell profile may wrap `canon` in a function that runs the binary and then another command and takes the second status, which masks every non-zero exit rather than only an absent verb. The binary exits 1 for an unknown subcommand and 1 for an ordinary refusal alike, so the record is the only signal that survives the wrapper.
59
63
 
@@ -159,7 +163,7 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
159
163
  | Check | What it reports |
160
164
  | ---------- | -------------------------------------------------------------------------------------------------------------------- |
161
165
  | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
162
- | Plan | A `## Run now` row whose Plan column carries no link, or one resolving to no file |
166
+ | Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line |
163
167
  | Mapping | A row or backlog line naming no task file, and a task file neither surface names |
164
168
  | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
165
169
  | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits |
@@ -168,6 +172,8 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
168
172
 
169
173
  Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
170
174
 
175
+ The Plan check reads the row and the task file both, because the two are written by different hands and only the task's own `Plan:` line reaches the archive. A row carrying a plan whose task states none is `plan-uncited`, and a pair naming two different plans is `plan-mismatched`. Both sides resolve against the board and against the project root before they compare, so a row writing `../plans/x.md` and a task writing `.canon/plans/x.md` name one file rather than two.
176
+
171
177
  Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A task file either surface names is accounted for, a file neither names is `row-missing`, and a file both name is `row-duplicated` for the reason a task in two groups is: it claims two things about itself and only one can hold. One check across both is what lets a task move between them without the move reading as a dropped file.
172
178
 
173
179
  A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name is still reported as reaching neither surface. A project carrying no `backlog.md` reads as an empty backlog rather than a refusal, which leaves the one-to-one mapping this check ran before the second surface existed.
@@ -108,12 +108,10 @@ A person points it at a private repository once and both verbs refuse until they
108
108
 
109
109
  `canon records size` reports what each of these folders holds, heaviest first, along with `.canon/tmp`. Each row carries the file count, the bytes, how many files were written in the last 7 and 30 days, and the dates of the least and most recently written one. Nothing fails on a number, because a record folder has no correct size. What the verb replaces is a reading somebody had to remember to take: the memory pen went from 44 entries to 236 between two counts made by hand a fortnight apart, and nothing reported the rate in between.
110
110
 
111
- A plan that ships is archived, never deleted. `canon:claude-docs` moves it to `.canon/plans/archive/` and retargets the task file's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. An archive sits inside the record folder it archives rather than beside it, so one ignore entry and one backed-folder entry cover a record and everything it has retired. The folder is gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
111
+ A plan that ships is archived, never deleted. `canon tasks archive` moves it to `.canon/plans/archive/` alongside the task it belonged to and retargets that task's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. An archive sits inside the record folder it archives rather than beside it, so one ignore entry and one backed-folder entry cover a record and everything it has retired. The folder is gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
112
112
 
113
113
  A branch review report takes the other route and is swept rather than archived. `claude-review` writes it to `.canon/review/branch/`, the session addressing it reads it once, and the durable record of what a review found is the comment `claude-pr-review` posts on the pull request, so `claude-docs` deletes any report whose branch is gone. The body that writes a report owns how long it lives, which leaves the shipping branch's own report on disk through the run that cites it and collects it a branch later. What that loses is a local-only review on a branch that never opened a pull request, which is why the report says so where a reader meets it.
114
114
 
115
- The plans sweep reads the whole board rather than the tasks the session touched. It is the one place the skill reaches past its own rule against editing a task file the session did not change, because a task that closed while an earlier run missed its archive is exactly what the sweep exists to clear. Reaching it is safe: the archive moves the plan and retargets the pointer in the same pass, so an untouched task ends up with a working link rather than a broken one.
116
-
117
115
  `canon:claude-docs` decides which task closed by reading the diff rather than the conversation. It resolves a merge base against `origin/main`, unions the committed diff with the working tree and untracked files, then matches unchecked outcomes on the board against what shipped. A task that shipped without ever being discussed still gets marked. Requirements, architecture, and design stay session-sourced, because a diff cannot carry a judgment.
118
116
 
119
117
  `.canon/tasks/` is gitignored and resolves at the main worktree root, so every session shares one board. One file per task is what keeps concurrent sessions from overwriting each other, since a gitignored board has no history to recover a lost write from. Its `index.md` is generated by a hook rather than by `bun run check`, because the whole-repo index walk skips gitignored folders.
@@ -126,7 +124,7 @@ Past that shape, it checks what a surviving row claims against what the tree hol
126
124
 
127
125
  `canon:claude-tasks` owns the two operations that bracket a task's life. It creates the file, holding the filename convention and the frontmatter contract so a malformed write cannot break the index for every sibling, and it moves a shipped task to `.canon/tasks/archive/`. Creation is where the origin invariant is enforced: every task names a plan, a groundwork folder, an intake folder, or an issue, since a task with no origin is either lost context or work nobody decided to do.
128
126
 
129
- Archiving a task leaves its plan alone, because `canon:claude-docs` owns the plans sweep and already holds the last-live-citation rule. That makes the order load-bearing, so the archive verb refuses the last task pointing at a live plan. The sweep only reaches tasks still in the live folder, and archiving that task first would strand the plan there with nothing citing it. A task whose plan a sibling still cites archives freely, because the sweep is correct to leave that plan where it is and a gate reading the folder instead would park every task sharing one plan behind a sweep that will never move it.
127
+ Archiving a task carries its plan with it, in the same act. The merge is what settles a plan, and the hook below reaches the archive with nobody watching, so a second call after it would be a second failure point leaving the task archived and the plan live. A task whose plan a sibling still cites archives on its own and leaves the plan live, since moving it on the first task to close strands every other pointer at a path that has gone.
130
128
 
131
129
  Nothing chained that archive until the `post-merge` git hook landed. Every earlier step fires from `canon:claude-autoship` or `canon:git-ship`, both of which finish while the pull request is still open, so a task archived there would close for work that may be abandoned. The board is gitignored, which rules out reading it from anywhere but the machine that pulled. The hook names the board's archive candidates and stays silent otherwise, including on a project with no board.
132
130
 
@@ -12,6 +12,8 @@ Claude Code auto-discovers the toolkit plugin from `claude/.claude-plugin/plugin
12
12
 
13
13
  `bun run bootstrap` installs this block. It owns the canonical copy in `scripts/core/bootstrap.sh`, appends it to `~/.zshrc` wrapped in the two marker comments below, and sets `TOOLKIT` to the cloned path. The markers are how a re-run recognizes its own block and skips.
14
14
 
15
+ Skipping means a re-run never updates a block already installed, so a shell that ran the bootstrap before the canonical block grew keeps whatever it got that day. To pick up an alias added since, delete the marked block and re-run the bootstrap, or paste the new lines into the block by hand.
16
+
15
17
  A block installed by hand before the managed one has no markers. Bootstrap detects that case through the `alias clp=` line instead, warns, and leaves the block untouched rather than appending a second copy. To switch over, delete the hand-rolled block and re-run the bootstrap.
16
18
 
17
19
  ```zsh
@@ -27,26 +29,34 @@ alias cls='cl --model sonnet'
27
29
  alias clp='claude --plugin-dir $TOOLKIT/claude'
28
30
  alias clpc='clp -c'
29
31
  alias clps='clp --model sonnet'
32
+ alias clpa='clp agents'
33
+ alias clpac='clpa --cwd .'
30
34
  # <<< canon aliases <<<
31
35
  ```
32
36
 
33
- The block sits after any `PATH` mutations and the `claude` CLI install. Zsh expands aliases recursively on the first word, so `clr`, `clc`, `clw`, and `cls` inherit their base through `cl`, and `clpc` and `clps` inherit `--plugin-dir` through `clp`. `$TOOLKIT` expands at invocation time, so updating the variable and re-sourcing reroutes all `clp` calls without touching the alias definitions.
37
+ The block sits after any `PATH` mutations and the `claude` CLI install. Zsh expands aliases recursively on the first word, so `clr`, `clc`, `clw`, and `cls` inherit their base through `cl`, and `clpc`, `clps`, and `clpa` inherit `--plugin-dir` through `clp`. `clpac` inherits it one level further, through `clpa`. `$TOOLKIT` expands at invocation time, so updating the variable and re-sourcing reroutes all `clp` calls without touching the alias definitions.
38
+
39
+ To opt out, delete the block between the two markers. Bootstrap re-adds it on the next run, so skip that step by running `bun install` and `bun link` yourself instead.
34
40
 
35
41
  ## What each one does
36
42
 
37
- `cl` through `cls` carry no explicit plugin dir. Use them inside the toolkit repository, where Claude Code auto-discovers the plugin from `claude/.claude-plugin/plugin.json`. Loading `--plugin-dir` on top of auto-discovery registers every skill twice and produces duplicate entries in the slash command list.
43
+ `cl`, `clw`, and `cls` carry no explicit plugin dir and start a fresh session. Use them inside the toolkit repository, where Claude Code auto-discovers the plugin from `claude/.claude-plugin/plugin.json`. Loading `--plugin-dir` on top of auto-discovery registers every skill twice and produces duplicate entries in the slash command list.
44
+
45
+ `clr` and `clc` resume a session rather than launching one, so auto-discovery never fires for them. A resume restores whatever plugin registration the session started with and recomputes nothing, which a bare `claude --resume` demonstrated by holding a stale registration through a full process restart until `--plugin-dir` was passed on the resume itself.
38
46
 
39
47
  - `cl`: plain session in the current directory
40
- - `clr`: opens the `/resume` picker scoped to the current directory. Trailing arguments filter by name. `clr auth` limits results to sessions containing "auth".
41
- - `clc`: jumps straight into the most recent session for the current directory. No picker. Faster than `clr` when the terminal closed and you want back into the same session.
48
+ - `clr`: opens the `/resume` picker scoped to the current directory. Trailing arguments filter by name. `clr auth` limits results to sessions containing "auth". Resuming does not reload the plugin, so reach for `clpc` when the session needs it.
49
+ - `clc`: jumps straight into the most recent session for the current directory. No picker. Faster than `clr` when the terminal closed and you want back into the same session. Resuming does not reload the plugin here either.
42
50
  - `clw`: creates a worktree under `.claude/worktrees/<name>/` on a fresh branch and starts a Claude Code session in it. Pass the worktree name as the trailing arg: `clw feat-auth`.
43
51
  - `cls`: pins the session to Sonnet instead of the default Opus. Use for routine work where Opus cost is not justified.
44
52
 
45
- `clp`, `clpc`, and `clps` bake in `--plugin-dir`. Use them outside the toolkit repository, where auto-discovery does not fire.
53
+ The `clp` family bakes in `--plugin-dir`. Use it outside the toolkit repository, where auto-discovery does not fire, and for any resume that needs the plugin.
46
54
 
47
55
  - `clp`: session with the toolkit plugin loaded explicitly
48
- - `clpc`: jumps straight into the most recent session for the current directory with the plugin loaded. The `clp` mirror of `clc`.
56
+ - `clpc`: jumps straight into the most recent session for the current directory with the plugin loaded. The `clp` mirror of `clc`, and the one resume that carries the plugin.
49
57
  - `clps`: `clp` pinned to Sonnet
58
+ - `clpa`: opens the agent view with the plugin loaded, covering background sessions from every directory
59
+ - `clpac`: the same view filtered to background sessions started under the current directory
50
60
 
51
61
  ## When to use which
52
62
 
@@ -58,7 +68,9 @@ Use `cls` or `clps` to save Opus usage on routine sessions. Switch mid-session w
58
68
 
59
69
  Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for fan-out rules.
60
70
 
61
- Use `clc` to resume the last session without a picker. Use `clr` when you have several sessions and need to pick by name or recency. Outside the toolkit repo, `clpc` is the same shortcut as `clc` with the plugin loaded.
71
+ Use `clc` to resume the last session without a picker. Use `clr` when you have several sessions and need to pick by name or recency. Reach for `clpc` over `clc` wherever the resumed session needs the plugin, inside the toolkit repository as well as outside it, since neither resume re-runs discovery.
72
+
73
+ Use `clpa` to see what is running across every directory, and `clpac` when only the current project matters.
62
74
 
63
75
  ## Why not a function
64
76
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.8.0",
4
+ "version": "4.8.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -36,12 +36,16 @@ alias cls='cl --model sonnet'
36
36
  alias clp='claude --plugin-dir \$TOOLKIT/claude'
37
37
  alias clpc='clp -c'
38
38
  alias clps='clp --model sonnet'
39
+ alias clpa='clp agents'
40
+ alias clpac='clpa --cwd .'
39
41
  $MARKER_CLOSE
40
42
  EOF
41
43
  }
42
44
 
43
45
  install_aliases() {
44
46
  log_step "Installing Claude Code shell aliases"
47
+ log_info "This step manages a marked alias block in $ZSHRC, the only file outside the repo this script writes."
48
+ log_info "Delete the block and its two marker comments to remove the aliases. Re-running never adds a second copy."
45
49
 
46
50
  if [ -f "$ZSHRC" ] && grep -qF "$MARKER_OPEN" "$ZSHRC"; then
47
51
  log_info "Aliases already present in $ZSHRC, skipping"
@@ -81,7 +81,9 @@ export function register(program: Command): void {
81
81
 
82
82
  tasks
83
83
  .command('archive')
84
- .description('Move a shipped task out of the board and clear its ordering')
84
+ .description(
85
+ 'Move a shipped task and its plan out of the board and clear its ordering',
86
+ )
85
87
  .argument('[task]', 'Task filename stem, as in v28.1-trigger-escalation')
86
88
  .helpOption('-h, --help', 'Show this help message')
87
89
  .option(
@@ -93,6 +95,10 @@ export function register(program: Command): void {
93
95
  .addHelpText(
94
96
  'after',
95
97
  [
98
+ '',
99
+ 'The task carries its plan with it when no other live task cites that',
100
+ 'plan, and the archived task keeps a working Plan: pointer at the new',
101
+ 'path. A plan several tasks share stays where it is.',
96
102
  '',
97
103
  'Exit codes:',
98
104
  ' 0 the task was archived',
@@ -812,6 +818,11 @@ function report(
812
818
  logStep('Archived')
813
819
  logRemove(relative(root, outcome.from))
814
820
  logAdd(relative(root, outcome.to))
821
+ if (outcome.plan) {
822
+ logRemove(relative(root, outcome.plan.from))
823
+ logAdd(relative(root, outcome.plan.to))
824
+ logInfo('retargeted the Plan: line')
825
+ }
815
826
  if (outcome.priorityRowRemoved) logInfo('cleared the ordering row')
816
827
  if (outcome.indexRegenerated) logInfo('regenerated index.md')
817
828
  outro()
@@ -839,5 +850,11 @@ function recordFor(
839
850
  to: relative(root, outcome.to),
840
851
  priorityRowRemoved: outcome.priorityRowRemoved,
841
852
  indexRegenerated: outcome.indexRegenerated,
853
+ plan: outcome.plan
854
+ ? {
855
+ from: relative(root, outcome.plan.from),
856
+ to: relative(root, outcome.plan.to),
857
+ }
858
+ : null,
842
859
  }
843
860
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises'
3
- import { join, relative, resolve } from 'node:path'
3
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path'
4
4
  import { regenOne } from '@/indexes/regen'
5
5
  import { isUnder } from '@/paths'
6
6
  import { recordDir, recordDirs } from '@/record-root'
@@ -43,7 +43,6 @@ export const ARCHIVE_REFUSALS = [
43
43
  'ambiguous',
44
44
  'no-outcomes',
45
45
  'open-outcomes',
46
- 'plan-unswept',
47
46
  'bad-input',
48
47
  ] as const
49
48
 
@@ -53,6 +52,12 @@ export type TaskSelector =
53
52
  | { readonly kind: 'stem'; readonly stem: string }
54
53
  | { readonly kind: 'pull-request'; readonly number: number }
55
54
 
55
+ /** A plan carried into the archive alongside the task that was its last citation. */
56
+ export interface PlanMove {
57
+ readonly from: string
58
+ readonly to: string
59
+ }
60
+
56
61
  export interface ArchiveSuccess {
57
62
  readonly ok: true
58
63
  readonly stem: string
@@ -60,6 +65,8 @@ export interface ArchiveSuccess {
60
65
  readonly to: string
61
66
  readonly priorityRowRemoved: boolean
62
67
  readonly indexRegenerated: boolean
68
+ /** Undefined when the task cited no live plan, or when another task still holds it. */
69
+ readonly plan: PlanMove | undefined
63
70
  }
64
71
 
65
72
  export interface ArchiveRefused {
@@ -142,16 +149,42 @@ export function readPullRequest(text: string): number | undefined {
142
149
  return match ? Number(match[1]) : undefined
143
150
  }
144
151
 
152
+ /**
153
+ * The `Plan:` line in either form, the link's target captured ahead of the bare
154
+ * path. Padding is spaces and tabs rather than `\s`, which spans a newline, so
155
+ * the match ends at the line and the retarget below cannot swallow the blank
156
+ * line that follows it.
157
+ */
158
+ const PLAN_PATTERN = /^Plan:[ \t]*(?:\[[^\]]*\]\(([^)]+)\)|(\S+))[ \t]*$/m
159
+
145
160
  /**
146
161
  * Reads the `Plan:` target out of a markdown link, falling back to the older
147
162
  * bare-path form. The path is returned as written, relative to the board.
148
163
  */
149
164
  export function readPlanTarget(text: string): string | undefined {
150
- const match = /^Plan:\s*(?:\[[^\]]*\]\(([^)]+)\)|(\S+))\s*$/m.exec(text)
165
+ const match = PLAN_PATTERN.exec(text)
151
166
  if (!match) return undefined
152
167
  return match[1] ?? match[2]
153
168
  }
154
169
 
170
+ /**
171
+ * Points the task's `Plan:` line at the plan's new home, as a markdown link
172
+ * whose text and target stay in step. The line is matched with the pattern the
173
+ * read above uses, so the archive rewrites exactly the line it parsed and never
174
+ * a second `Plan:` a task displays inside a fenced sample.
175
+ *
176
+ * The replacement is built by a function rather than passed as a string,
177
+ * because `$&` and its siblings are substitution sequences inside a replacement
178
+ * string. A plan filename carrying one would write a path nobody typed, and
179
+ * `.canon/plans/` is gitignored, so nothing recovers the pointer it replaced.
180
+ */
181
+ export function retargetPlanLine(text: string, target: string): string {
182
+ const name = basename(target)
183
+ const label = name.endsWith('.md') ? name.slice(0, -'.md'.length) : name
184
+
185
+ return text.replace(PLAN_PATTERN, () => `Plan: [${label}](${target})`)
186
+ }
187
+
155
188
  /**
156
189
  * Drops the archived task's row from the ordering table. Rows are matched by
157
190
  * the link they carry rather than by a line pattern, because a row holds links
@@ -476,28 +509,23 @@ export async function archiveTask(
476
509
  )
477
510
  }
478
511
 
479
- const planTarget = readPlanTarget(text)
480
- const livePlan = planTarget && resolveLivePlan(planTarget, dir, root)
481
-
482
- // A live plan is unswept only when nothing else on the board holds it. A plan
483
- // several tasks share stays live by design, so refusing on the folder alone
484
- // parked every one of those tasks behind a sweep that was right to decline.
485
- if (livePlan) {
486
- const shared = await otherTasksCitingPlan(dir, root, livePlan, stem)
512
+ const plan = await planToArchive(dir, root, stem, text)
513
+ const destination = archiveDir(root)
514
+ const to = join(destination, `${stem}.md`)
487
515
 
488
- if (shared.length === 0) {
489
- return refuse(
490
- 'plan-unswept',
491
- `${stem} is the last task pointing at a live plan. Run /claude-docs to sweep it first, then archive.`,
492
- [planTarget],
493
- )
494
- }
516
+ // The plan moves first so the line written below describes a file already at
517
+ // its new path. Writing the retarget first and failing the move would leave a
518
+ // pointer at a folder holding nothing, and `.canon/plans/` is gitignored, so
519
+ // no history recovers the target it named.
520
+ if (plan) {
521
+ await mkdir(dirname(plan.to), { recursive: true })
522
+ await rename(plan.from, plan.to)
495
523
  }
496
524
 
497
- const destination = archiveDir(root)
498
525
  await mkdir(destination, { recursive: true })
499
- const to = join(destination, `${stem}.md`)
500
526
  await rename(from, to)
527
+ if (plan)
528
+ await writeFile(to, retargetPlanLine(text, linkTo(destination, plan.to)))
501
529
 
502
530
  const priorityRowRemoved = await clearPriorityRow(dir, stem)
503
531
  const regen = await regenOne(dir, { dryRun: false })
@@ -509,9 +537,52 @@ export async function archiveTask(
509
537
  to,
510
538
  priorityRowRemoved,
511
539
  indexRegenerated: regen.action === 'written',
540
+ plan,
512
541
  }
513
542
  }
514
543
 
544
+ /**
545
+ * The plan this task carries into the archive with it, or nothing. The merge is
546
+ * what settles a plan, and the hook reaches this with nobody watching, so the
547
+ * move sits inside the archive rather than in a second call that could leave
548
+ * the task archived and the plan live.
549
+ *
550
+ * A plan another live task still cites stays where it is. Moving it on the
551
+ * first task to close strands every other pointer at a path that has gone, and
552
+ * the sibling has no history behind it to repair the line from.
553
+ *
554
+ * A target resolving to no file yields nothing too. A pointer somebody typed
555
+ * wrong is not a plan to move, and refusing the whole archive over it would
556
+ * park the board behind a repair the merge cannot make.
557
+ */
558
+ async function planToArchive(
559
+ dir: string,
560
+ root: string,
561
+ stem: string,
562
+ text: string,
563
+ ): Promise<PlanMove | undefined> {
564
+ const target = readPlanTarget(text)
565
+ const live = target && resolveLivePlan(target, dir, root)
566
+ if (!live || !existsSync(live)) return undefined
567
+
568
+ const shared = await otherTasksCitingPlan(dir, root, live, stem)
569
+ if (shared.length > 0) return undefined
570
+
571
+ return {
572
+ from: live,
573
+ to: join(recordDir(root, PLANS, ARCHIVE), basename(live)),
574
+ }
575
+ }
576
+
577
+ /**
578
+ * The archived plan as the archived task cites it. Both halves land a folder
579
+ * deeper than the live pair, so the link is measured between the two
580
+ * destinations rather than written as the `../plans/` the live task carried.
581
+ */
582
+ function linkTo(taskDir: string, plan: string): string {
583
+ return relative(taskDir, plan).split(sep).join('/')
584
+ }
585
+
515
586
  async function clearPriorityRow(dir: string, stem: string): Promise<boolean> {
516
587
  const path = join(dir, 'priority.md')
517
588
  if (!existsSync(path)) return false
@@ -5,6 +5,7 @@ import {
5
5
  archiveDir,
6
6
  isReservedStem,
7
7
  readOutcomes,
8
+ readPlanTarget,
8
9
  readPullRequest,
9
10
  tasksDir,
10
11
  } from '@/tasks/archive'
@@ -34,6 +35,8 @@ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
34
35
  export const FINDING_KINDS = [
35
36
  'plan-unstated',
36
37
  'plan-unresolved',
38
+ 'plan-uncited',
39
+ 'plan-mismatched',
37
40
  'task-unresolved',
38
41
  'row-missing',
39
42
  'row-duplicated',
@@ -612,6 +615,78 @@ function checkPlans(
612
615
  return findings
613
616
  }
614
617
 
618
+ /**
619
+ * Compares the two places one task's plan is written down. The `## Run now` row
620
+ * carries a `Plan` column and the task file carries its own `Plan:` line, and
621
+ * the archive reads the second while an operator reads the first, so a pair
622
+ * that disagrees settles the wrong plan on the merge.
623
+ *
624
+ * Both sides resolve before they compare. A row writing `../plans/x.md` and a
625
+ * task writing `.canon/plans/x.md` name one file, and comparing the strings
626
+ * would report every such pair as a mismatch.
627
+ */
628
+ async function checkPlanAgreement(
629
+ rows: readonly BoardRow[],
630
+ dir: string,
631
+ root: string,
632
+ ): Promise<Finding[]> {
633
+ const ready = rows.filter((row) => row.group === 'Run now' && row.plan)
634
+
635
+ const found = await Promise.all(
636
+ ready.map(async (row) => planDisagreement(row, dir, root)),
637
+ )
638
+
639
+ return found.filter((finding): finding is Finding => finding !== undefined)
640
+ }
641
+
642
+ async function planDisagreement(
643
+ row: BoardRow,
644
+ dir: string,
645
+ root: string,
646
+ ): Promise<Finding | undefined> {
647
+ const subject = row.stem ?? row.label
648
+ if (!row.stem || !row.plan) return undefined
649
+
650
+ const file = join(dir, `${row.stem}.md`)
651
+ if (!existsSync(file)) return undefined
652
+
653
+ const target = readPlanTarget(await readFile(file, 'utf8'))
654
+ if (!target) {
655
+ return {
656
+ kind: 'plan-uncited',
657
+ group: 'Run now',
658
+ subject,
659
+ message: `is rowed against ${row.plan}, and the task file carries no Plan: line, so the archive settles no plan when it ships.`,
660
+ }
661
+ }
662
+
663
+ const rowed = planPath(row.plan, dir, root)
664
+ const cited = planPath(target, dir, root)
665
+ if (rowed === cited) return undefined
666
+
667
+ return {
668
+ kind: 'plan-mismatched',
669
+ group: 'Run now',
670
+ subject,
671
+ message: `is rowed against ${row.plan} and cites ${target} in its own Plan: line. One task names one plan.`,
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Where a plan pointer lands, resolved against the board and against the
677
+ * project root the way the archive resolves the same line. Neither base
678
+ * existing leaves the board-relative reading, so two pointers at one absent
679
+ * file still compare equal and the mismatch check reports nothing.
680
+ */
681
+ function planPath(target: string, dir: string, root: string): string {
682
+ const path = target.split('#')[0] || target
683
+ const fromBoard = resolve(dir, path)
684
+ if (existsSync(fromBoard)) return fromBoard
685
+
686
+ const fromRoot = resolve(root, path)
687
+ return existsSync(fromRoot) ? fromRoot : fromBoard
688
+ }
689
+
615
690
  /**
616
691
  * The half of the `## Run now` test a person cannot check by eye. Two rows a
617
692
  * worker may be handed at once must touch disjoint files, and the `Touches`
@@ -984,6 +1059,7 @@ export async function validateBoard(
984
1059
  ...shapeFindings,
985
1060
  ...checkMapping(rows, backlog, stems, dir),
986
1061
  ...checkPlans(rows, dir, root),
1062
+ ...(await checkPlanAgreement(rows, dir, root)),
987
1063
  ...checkCollisions(rows),
988
1064
  ...checkOrdinals(rows),
989
1065
  ...parked.findings,
@@ -47,7 +47,7 @@ The handoff takes one file per session for the reason a task does. A single shar
47
47
 
48
48
  The catalog is the one reader that filters nothing, so it carries a row per sibling alongside the tasks. That is what a folder catalog is for, and the handoffs are what make it worth stating: a board accumulates one row per session that ever wrote one, with nothing pruning them. Anything reading the catalog as the backlog therefore does its own filtering, and a reader that takes every row as a task reports the handoffs as queued work.
49
49
 
50
- The `claude-tasks` skill creates and archives task files. `claude-docs` marks outcomes `[x]` in an existing file and sweeps the plans those tasks cite. Neither does the other's job.
50
+ The `claude-tasks` skill creates and archives task files, and the archive carries the task's plan with it. `claude-docs` marks outcomes `[x]` in an existing file. Neither does the other's job.
51
51
 
52
52
  ## Ordering
53
53
 
@@ -247,8 +247,8 @@ The archive nests inside `.canon/tasks/` rather than sitting beside it as a flat
247
247
 
248
248
  One destination rather than a per-project choice is what lets the move happen without asking. It mirrors the plans archive at `.canon/plans/archive/`, sitting inside the folder it archives the same way, and it inherits the board's own ignore entry rather than needing one of its own. The cost is that an archived task does not appear in diffs, which is the cost the live board already carries.
249
249
 
250
- Archiving a task does not archive its plan. `claude-docs` owns the plans sweep and moves a plan only when the closing task is its last live citation, so the sweep runs before the archive rather than after it. The sweep finds its work by scanning the live folder, and a task archived first is beyond its reach for good, leaving the plan with no live task citing it and an archived task pointing at a path nothing will retarget.
250
+ Archiving a task archives its plan alongside it, when the closing task is that plan's last live citation. The archived task's `Plan:` line is retargeted at `../../plans/archive/feature-<slug>.md`, a folder deeper than the live task wrote it, so a completed task still leads to the reasoning behind it. A plan several tasks share stays live and the task archives anyway, since moving it on the first task to close strands every sibling's pointer at a path that has gone.
251
251
 
252
- The `claude-docs` sweep states that ordering in its own body rather than reading it back from a command, which is a duplication accepted with a reason rather than an oversight. A skill reaches a target the moment it merges and the CLI reaches one only when a release publishes, so a body calling a verb the installed `canon` predates gets no record back and sweeps nothing. The two spellings therefore have to agree by hand until a release carries the verb, and the failure they guard against is a plan stranded by the form its citation was written in.
252
+ One act rather than two is what makes the pair safe. The merge is the event that settles a plan, and a `post-merge` hook reaching the archive with nobody watching cannot act on a warning, so a second call after it would be a second failure point leaving the task archived and the plan live.
253
253
 
254
- A task with an open outcome stays on the board. Close it, or cut it from the task when the work is being abandoned, so what was dropped is recorded rather than inferred from an archived file. The sweep is gated on the same condition, so archiving around an open outcome also leaves the plan behind.
254
+ A task with an open outcome stays on the board, and so does its plan. Close it, or cut it from the task when the work is being abandoned, so what was dropped is recorded rather than inferred from an archived file.