@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 +55 -41
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-docs/REQUIREMENT.md +2 -2
- package/claude/skills/claude-docs/SKILL.md +7 -36
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +13 -8
- package/claude/skills/claude-tasks/SKILL.md +4 -5
- package/claude/skills/claude-worker/SKILL.md +2 -2
- package/docs/agents/commands.md +1 -1
- package/docs/agents/overview.md +3 -3
- package/docs/agents/tasks.md +10 -4
- package/docs/ai-workflow.md +2 -4
- package/docs/zshrc-aliases.md +19 -7
- package/package.json +1 -1
- package/scripts/core/bootstrap.sh +4 -0
- package/src/commands/tasks.ts +18 -1
- package/src/tasks/archive.ts +91 -20
- package/src/tasks/validate.ts +76 -0
- package/standards/tasks.md +4 -4
package/README.md
CHANGED
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
[](https://github.com/erclx/canon/actions/workflows/verify.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
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
|

|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
211
|
+
## Step 9: sweep consumed receipts
|
|
212
212
|
|
|
213
|
-
Sweep
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
205
|
-
reads `planner-` for the reason the worker's reads `worker-`,
|
|
206
|
-
marks the role of the session it names rather than the one
|
|
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]
|
|
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]
|
|
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.
|
|
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
|
|
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.
|
package/docs/agents/commands.md
CHANGED
|
@@ -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`) |
|
package/docs/agents/overview.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/docs/agents/tasks.md
CHANGED
|
@@ -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`,
|
|
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
|
-
|
|
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`
|
|
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
|
|
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.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/docs/zshrc-aliases.md
CHANGED
|
@@ -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 `
|
|
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`
|
|
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
|
|
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.
|
|
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
|
@@ -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"
|
package/src/commands/tasks.ts
CHANGED
|
@@ -81,7 +81,9 @@ export function register(program: Command): void {
|
|
|
81
81
|
|
|
82
82
|
tasks
|
|
83
83
|
.command('archive')
|
|
84
|
-
.description(
|
|
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
|
}
|
package/src/tasks/archive.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
480
|
-
const
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
package/src/tasks/validate.ts
CHANGED
|
@@ -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,
|
package/standards/tasks.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|