@erclx/canon 4.21.0 → 4.22.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
@@ -61,9 +61,9 @@ Each domain has a canonical source in this repo and a thin install or sync CLI o
61
61
 
62
62
  Scaffolding your first project? Start with target projects, then the AI workflow loop. Everything else answers questions that arrive later.
63
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
64
+ - [AI workflow](docs/workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
65
+ - [Operating model](docs/workflow/operating-model.md): orchestrator, planner, and worker roles for building across parallel sessions
66
+ - [Visual design workflow](docs/workflow/visual-design-workflow.md): tiered guide for design and wireframe authoring
67
67
  - [Target projects](docs/target-projects.md): scaffold, add a domain later, sync upstream drift
68
68
  - [Agents](docs/agents/index.md): CLI flags, exit codes, and JSON output shapes
69
69
  - [Docs index](docs/index.md): every reference doc in this repo
@@ -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. 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.
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/workflow/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
 
@@ -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.21.0",
4
+ "version": "4.22.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -7,7 +7,7 @@ description: Reviews all changes since main for bugs, edge cases, and logic flaw
7
7
 
8
8
  ## Guards
9
9
 
10
- - Resolve the base ref first, per Diff baseline below. If the staged set, the branch set, and the working set are all empty, stop: `✅ No changes to review.` A guard reading bare local `main` stops the skill on `main` before it ever reaches Step 2.
10
+ - Resolve the base ref first, per Diff baseline below. If `git diff <base>` and the untracked listing are both empty, stop: `✅ No changes to review.` Those two are the sets Step 2 reads, and the range already carries the staged and the unstaged work the guard used to test on its own. A guard reading bare local `main` stops the skill on `main` before it ever reaches Step 2.
11
11
 
12
12
  ## Diff baseline
13
13
 
@@ -19,12 +19,9 @@ git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/n
19
19
 
20
20
  Prefer `origin/main` over local `main`. On `main` itself the local ref resolves to HEAD, so every committed change drops out of the set and the skill reports a clean branch rather than admitting it cannot see the work.
21
21
 
22
- The baseline is unusable in two cases:
22
+ The baseline is unusable in one case: no merge base resolves against either ref. Substitute `HEAD` and lead the report with `⚠ Baseline unusable. Reviewed the uncommitted set only.`, so a clean summary is never read as a clean branch. That substitution costs the committed half, because `git diff HEAD` reaches the staged and the unstaged work and nothing behind it.
23
23
 
24
- - No merge base resolves against either ref.
25
- - The base equals HEAD, whichever ref resolved it. Nothing is committed ahead of the base to compare against. This is the ordinary shape on `main`, and on a feature branch before its first commit.
26
-
27
- An unusable baseline costs only the committed half. `git diff <base> HEAD` is empty by definition once the base equals HEAD, while the staged set and `git diff HEAD` still report work at correct scope. Review those and lead the report with `⚠ Baseline unusable. Reviewed the uncommitted set only.`, so a clean summary is never read as a clean branch.
24
+ The base equalling HEAD is a usable baseline rather than the second case it used to be. It means nothing is committed ahead of the base, which is the ordinary shape on `main` and on a feature branch before its first commit, and `git diff <base>` degenerates there to `git diff HEAD` and reads the branch whole. Warning on it would tell a reader their review was partial when it had covered everything.
28
25
 
29
26
  ## Step 1: read context
30
27
 
@@ -41,24 +38,20 @@ Coding standards from `.claude/rules/` are auto-loaded by Claude Code. Always-on
41
38
  Resolve the base ref per Diff baseline above, then run these in parallel from the project root:
42
39
 
43
40
  ```bash
44
- git diff --staged
45
- ```
46
-
47
- ```bash
48
- git diff --staged --name-only
41
+ git diff <base>
49
42
  ```
50
43
 
51
44
  ```bash
52
- git diff <base> HEAD
45
+ git diff --name-only <base>
53
46
  ```
54
47
 
55
48
  ```bash
56
- git diff <base> HEAD --name-only
49
+ git ls-files --others --exclude-standard
57
50
  ```
58
51
 
59
- If `git diff --staged` is non-empty, use it as the diff scope and use the `--staged --name-only` list as the file list. Otherwise use `git diff <base> HEAD` and its name-only list.
52
+ Use `git diff <base>` as the diff scope. It compares the base against the working tree, so the committed, staged, and unstaged halves arrive in one range and no selection between them is made. Take the file list as the name-only output plus the untracked listing, which covers a file git has never tracked and therefore no diff can reach.
60
53
 
61
- When the baseline is unusable, substitute `git diff HEAD` and `git diff HEAD --name-only` for the branch pair. Never substitute the whole tree for a missing baseline.
54
+ When the baseline is unusable, substitute `HEAD` for `<base>` in both diff commands. Never substitute the whole tree for a missing baseline.
62
55
 
63
56
  ## Step 3: read changed files
64
57
 
@@ -10,7 +10,17 @@ description: How canon docs resolves the toolkit's own reference surface from an
10
10
  - `canon docs list [--json]` lists the downstream catalog: the consumer-facing `docs/` surface plus per-domain narrative from `.claude/context/` when that root is present. Toolkit-internal context entries (`ci`, `development`, `sandbox`) are dropped. From a registry install the context section is absent rather than empty.
11
11
  - `canon docs <topic>` prints one doc to stdout, resolved by exact name from `docs/` first, then `.claude/context/`. Any doc the install carries is reachable by name, including the toolkit-internal topics the list omits.
12
12
 
13
- A domain too large for one file splits into `<domain>/` with a generated `index.md`, and both verbs name it by the folder. `canon docs <domain>` prints that index, which is the catalog routing to the sub-area files, and the listing describes it from the index's `subtitle` where a sibling file supplies `description`. A sibling file wins over a folder of the same name. A folder carrying no `index.md` is absent from both, since a catalog is what makes the sub-areas reachable.
13
+ ## How a name resolves
14
+
15
+ A domain too large for one file splits into `<domain>/` with a generated `index.md`, and both verbs name it by the folder. `canon docs <domain>` prints that index, which is the catalog routing to the sub-area files, and the listing describes it from the index's `subtitle` where a sibling file supplies `description`. A folder carrying no `index.md` is absent from both, since a catalog is what makes the sub-areas reachable, and so are the files inside it.
16
+
17
+ A sub-area file resolves by its bare name too, so `canon docs operating-model` reaches `docs/workflow/operating-model.md`. Three spellings are tried in order, being a sibling file, a folder of that name, then a sub-area file one level down, and each spelling is tried across both roots before the next one runs. A sibling file therefore wins over a folder of the same name, and both win over a sub-area file. Reading the folder last is what keeps the sub-area spelling from changing any name that resolved before it existed.
18
+
19
+ A bare name carried by two folders resolves to neither. Eight context folders hold an `overview.md`, so `canon docs overview` names the available topics and exits 1 rather than answering with whichever folder sorts first.
20
+
21
+ `canon docs list` stops at the folder where `canon docs <topic>` goes on to name each file inside it. The listing is the downstream catalog rather than the index of what a name reaches, so a sub-area file appears there only where it declares a target-facing `category` of its own, and no sub-area file of the context root appears at all. A name that resolves and does not list is the divergence the toolkit-internal topics already carry.
22
+
23
+ ## Output
14
24
 
15
25
  Data prints to stdout and the frame to stderr, so `canon docs <topic> > out.md` captures clean markdown. With no topic and no verb, `canon docs` runs `list`. An unknown topic names the available topics on stderr and exits 1.
16
26
 
package/docs/index.md CHANGED
@@ -9,15 +9,9 @@ One-line reference for each doc in this folder.
9
9
 
10
10
  ## Agent surface
11
11
 
12
- - [AI workflow](ai-workflow.md): Overarching AI workflow across domains
13
12
  - [Target projects](target-projects.md): Scaffold, add domains later, and sync upstream drift in a toolkit-managed project
14
13
 
15
- ## Workflow
16
-
17
- - [Operating model](operating-model.md): Orchestrator, planner, and worker roles for building across parallel sessions
18
- - [Visual design workflow](visual-design-workflow.md): Tiered guide for design and wireframe authoring with Claude Code
19
- - [Zshrc aliases for Claude Code](zshrc-aliases.md): Shell aliases that shorten common Claude Code invocations
20
-
21
14
  ## Sub-catalogs
22
15
 
23
16
  - [Agents](agents/index.md): CLI catalog and invocation rules for agents, split by command domain. Start with overview.
17
+ - [Workflow](workflow/index.md): How a person and their agents run a feature, from the design tier through the parallel-session loop to the shell that launches them.
@@ -71,7 +71,7 @@ Scaffold installs tooling and seeds. It does not fill the planning docs or the d
71
71
  1. Fill `.claude/REQUIREMENTS.md` and `.claude/ARCHITECTURE.md`. The seed provides the files, the scope and decisions are yours to write.
72
72
  2. For a UI project, invoke `canon:claude-design-extract` to draft `.claude/DESIGN.md`. With no UI code yet it takes the greenfield path and proposes tokens from the requirements and a `## Personality` section. Skip for non-UI projects.
73
73
  3. Optionally invoke `canon:claude-diagram` to draft entries under `.canon/diagrams/` from the architecture and the requirements. One file per diagram kind, so a later refresh of one kind leaves the others untouched. It renders each diagram it writes to verify the layout, which downloads the Mermaid CLI on first use and takes about 15 seconds.
74
- 4. Start the feature loop. See [AI workflow](ai-workflow.md) for the per-feature sequence.
74
+ 4. Start the feature loop. See [AI workflow](workflow/ai-workflow.md) for the per-feature sequence.
75
75
 
76
76
  A machine without a renderer still gets the diagrams and is told which check was skipped.
77
77
 
@@ -286,6 +286,6 @@ Sync also refuses a target whose working tree is dirty, so commit or stash befor
286
286
  ## Related
287
287
 
288
288
  - [agents](agents/index.md): CLI flags, exit codes, and JSON output shapes
289
- - [AI workflow](ai-workflow.md): feature-development loop inside a toolkit-managed project
289
+ - [AI workflow](workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
290
290
  - [tooling](../.claude/context/tooling.md), [governance](../.claude/context/governance/index.md), [claude plugin](../.claude/context/claude-plugin/index.md), [indexes](../.claude/context/indexes.md), [snippets](../.claude/context/snippets.md), [standards](../.claude/context/standards/index.md): per-domain mechanics
291
291
  - [sandbox](../.claude/context/sandbox/index.md): scenario catalog for verifying domain flows
@@ -33,7 +33,7 @@ Project docs split across two roots at the project root, on one mechanical line:
33
33
 
34
34
  A project scaffolded before the move keeps its records under `.claude/`, and every command reads either root. `canon migrate records` moves one project across and repoints what cites it, and `canon migrate record-tree` follows it to reach the citations inside the records themselves, which the first verb passes over because it enumerates through git.
35
35
 
36
- Three tiers of context load with different cost: always-loaded (root `CLAUDE.md`, `.claude/REQUIREMENTS.md`, `.claude/ARCHITECTURE.md`), path-scoped lazy (`.claude/rules/<scope>.md` with `paths:` glob), and on-demand lookup (`.claude/context/<domain>.md`, or `.claude/context/<domain>/` once a domain outgrows one file, discovered via `.claude/context/index.md`). See [the context model](../.claude/context/context-model.md) for the full picture.
36
+ Three tiers of context load with different cost: always-loaded (root `CLAUDE.md`, `.claude/REQUIREMENTS.md`, `.claude/ARCHITECTURE.md`), path-scoped lazy (`.claude/rules/<scope>.md` with `paths:` glob), and on-demand lookup (`.claude/context/<domain>.md`, or `.claude/context/<domain>/` once a domain outgrows one file, discovered via `.claude/context/index.md`). See [the context model](../../.claude/context/context-model.md) for the full picture.
37
37
 
38
38
  Run `canon init` to seed the `.claude/` directory, a root `CLAUDE.md` file, and `.claude/rules/` in one pass. `canon init` chains claude init and governance install. Claude Code auto-loads every file in `.claude/rules/` at session start, applying always-on rules unconditionally and path-scoped rules to files matching their `paths:` glob.
39
39
 
@@ -41,7 +41,7 @@ Run `canon init` to seed the `.claude/` directory, a root `CLAUDE.md` file, and
41
41
 
42
42
  ### Bootstrap a new project
43
43
 
44
- See [target projects](target-projects.md) for the scaffold decision, core domains and skips, and the full lifecycle across scaffold, add-a-domain-later, and upstream sync.
44
+ See [target projects](../target-projects.md) for the scaffold decision, core domains and skips, and the full lifecycle across scaffold, add-a-domain-later, and upstream sync.
45
45
 
46
46
  ### New feature
47
47
 
@@ -94,7 +94,7 @@ Before a handoff, the orchestrator checks the plan against the tree rather than
94
94
 
95
95
  A constraint naming a track in flight carries the same problem past the handoff, so the block opens with the commit it was measured against. A worker re-tests before honoring one, fetching and then logging that commit against `origin/main` over the paths the constraint names, and any merge there means the track landed and the constraint is dead. An unstamped block reads as unverified rather than as live, which covers every plan written before the rule.
96
96
 
97
- `.canon/plans/`, `.canon/review/`, and `.canon/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. A session inside a worktree reads them directly, since the file-editing tools refuse a main-root path but `Read` resolves normally. It writes a whole file through the shell and makes a change inside an existing file through a `canon` verb, which resolves the main root in-process. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
97
+ `.canon/plans/`, `.canon/review/`, and `.canon/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. A session inside a worktree reads them directly, since the file-editing tools refuse a main-root path but `Read` resolves normally. It writes a whole file through the shell and makes a change inside an existing file through a `canon` verb, which resolves the main root in-process. See [Claude Code and git worktrees](../../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
98
98
 
99
99
  The plan's shape is fixed by `standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
100
100
 
@@ -104,7 +104,7 @@ An execution that picks other than the suggestion rewrites the `- Suggested:` li
104
104
 
105
105
  `canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Nine of them are backed: `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, each carrying whatever it has archived inside it. The history lives in a second git directory at `.canon/.records.git` with `.canon/` as its work tree, so every path a task file cites stays where it is.
106
106
 
107
- A person points it at a private repository once and both verbs refuse until they have, and `push` refuses when that origin is also a remote of the project, since the payload is the memory pen and the groundwork trails. `.husky/post-merge` runs the push after its archive loop, on every merge rather than only on one that closed a task. See [records](agents/records.md) for the refusal table.
107
+ A person points it at a private repository once and both verbs refuse until they have, and `push` refuses when that origin is also a remote of the project, since the payload is the memory pen and the groundwork trails. `.husky/post-merge` runs the push after its archive loop, on every merge rather than only on one that closed a task. See [records](../agents/records.md) for the refusal table.
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
 
@@ -151,7 +151,7 @@ Markdown under one states what an agent does, so a branch touching it reaches re
151
151
 
152
152
  An empty changed-file list stops the chain rather than counting as prose-only. The filename test passes vacuously on an empty set, which routed a branch past review instead of through it.
153
153
 
154
- `canon autoship classify` answers that decision now, and the chain branches on the record it returns rather than on a session applying the list above. Three runs read past the list while it was prose, the last of them a driven arm that staged a file the list names and shipped a draft pull request with no review. The verb takes the names the chain already computed, so no second diff baseline resolves, and it names the file and the test that decided. [Review classification](agents/review-classification.md) carries the record shape and the exit codes.
154
+ `canon autoship classify` answers that decision now, and the chain branches on the record it returns rather than on a session applying the list above. Three runs read past the list while it was prose, the last of them a driven arm that staged a file the list names and shipped a draft pull request with no review. The verb takes the names the chain already computed, so no second diff baseline resolves, and it names the file and the test that decided. [Review classification](../agents/review-classification.md) carries the record shape and the exit codes.
155
155
 
156
156
  The list stays written in the skill body as the fallback for a target whose installed CLI predates the verb, since the two ship at different speeds. That fallback is never a skip: failing open is the defect the verb closes, so an absent subcommand routes to review rather than past it.
157
157
 
@@ -187,7 +187,7 @@ Before the first feature session on a UI-heavy project, pick a design tier. The
187
187
 
188
188
  ## Skills
189
189
 
190
- Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios above with the lifecycle [target projects](target-projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
190
+ Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios above with the lifecycle [target projects](../target-projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
191
191
 
192
192
  This section is the corpus the coverage claim is measured against: every name `canon claude skills list --names` reports takes exactly one row here. A skill serving two moments sits at the earlier one, and mentions elsewhere in this file are prose rather than routing.
193
193
 
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Workflow
3
+ subtitle: How a person and their agents run a feature, from the design tier through the parallel-session loop to the shell that launches them.
4
+ ---
5
+
6
+ # Workflow
7
+
8
+ How a person and their agents run a feature, from the design tier through the parallel-session loop to the shell that launches them.
9
+
10
+ ## Agent surface
11
+
12
+ - [AI workflow](ai-workflow.md): Overarching AI workflow across domains
13
+
14
+ ## Workflow
15
+
16
+ - [Operating model](operating-model.md): Orchestrator, planner, and worker roles for building across parallel sessions
17
+ - [Visual design workflow](visual-design-workflow.md): Tiered guide for design and wireframe authoring with Claude Code
18
+ - [Zshrc aliases for Claude Code](zshrc-aliases.md): Shell aliases that shorten common Claude Code invocations
@@ -12,7 +12,7 @@ cross-feature call and reviews. Planning runs there or in a session of its own.
12
12
  Cold worker sessions build. The human launches workers and merges.
13
13
 
14
14
  This page covers the roles and the loop. For the worktree mechanism (isolation, merge
15
- order, port collisions), see [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md).
15
+ order, port collisions), see [Claude Code and git worktrees](../../wiki/claude/claude-worktrees.md).
16
16
 
17
17
  ## Three roles
18
18
 
@@ -99,7 +99,7 @@ becomes the commit message and the record on the trunk once the branch is gone.
99
99
  `canon pr key-changes` compares the paths the body's Key Changes claims against
100
100
  its own changed-file list, and a claim the diff does not carry is a finding on
101
101
  the body rather than on a file. See
102
- [Key Changes bijection](agents/key-changes.md).
102
+ [Key Changes bijection](../agents/key-changes.md).
103
103
 
104
104
  ## The review channel
105
105
 
@@ -113,7 +113,7 @@ which trails the branch ref by up to a minute after a push and says nothing
113
113
  about the trail. `canon pr head` resolves the tip the review pass scopes its
114
114
  delta against, and `canon pr checks` reports the runs belonging to that tip, so a
115
115
  follow-up push cannot be read as green off the predecessor's completed run. See
116
- [Head-sensitive pull request reads](agents/pr-reads.md).
116
+ [Head-sensitive pull request reads](../agents/pr-reads.md).
117
117
 
118
118
  What the session channel carries is the handback instruction and the worker's
119
119
  reply to it, which is a notification layer over a record that stays on the PR. A
@@ -248,11 +248,11 @@ A dev server, an end-to-end run, and a screenshot run alongside each other on a
248
248
  web stack, since every worktree derives its own port. Singleton resources (one
249
249
  local model server, one GPU) still serialize, as does any port a stack fixes by
250
250
  hand. See
251
- [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for merge order and the
251
+ [Claude Code and git worktrees](../../wiki/claude/claude-worktrees.md) for merge order and the
252
252
  port-collision detail.
253
253
 
254
254
  ## Related
255
255
 
256
- - [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the isolation and fan-out mechanics
257
- - [Claude Code subagents](../wiki/claude/claude-subagents.md) for in-session parallelism without worktrees
256
+ - [Claude Code and git worktrees](../../wiki/claude/claude-worktrees.md) for the isolation and fan-out mechanics
257
+ - [Claude Code subagents](../../wiki/claude/claude-subagents.md) for in-session parallelism without worktrees
258
258
  - `.claude/context/claude-plugin/skill-strategy.md` for how the skills in the loop are categorized
@@ -8,9 +8,9 @@ category: Workflow
8
8
 
9
9
  Three tiers cover the range from prose-only design docs to a fully graphical design source of truth. Pick one per project based on how UI-heavy the work is, whether stakeholders review visuals, and whether a designer is involved. Tiers stack, so moving up does not invalidate work done at a lower tier.
10
10
 
11
- The tier framework sits alongside [Claude Design](../wiki/claude/claude-design.md), [visual wireframes](../wiki/tools/visual-wireframes.md), [community skills and plugins](../wiki/tools/community-skills.md), and [community MCP servers](../wiki/tools/community-mcp-servers.md). Those pages catalog the tooling. This page decides when to reach for what.
11
+ The tier framework sits alongside [Claude Design](../../wiki/claude/claude-design.md), [visual wireframes](../../wiki/tools/visual-wireframes.md), [community skills and plugins](../../wiki/tools/community-skills.md), and [community MCP servers](../../wiki/tools/community-mcp-servers.md). Those pages catalog the tooling. This page decides when to reach for what.
12
12
 
13
- Two tools anchor tier 1 and tier 2. [Stitch](../wiki/tools/stitch.md) is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../wiki/claude/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
13
+ Two tools anchor tier 1 and tier 2. [Stitch](../../wiki/tools/stitch.md) is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../../wiki/claude/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
14
14
 
15
15
  ## Tier 0: prose only
16
16
 
@@ -31,7 +31,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
31
31
  ### Tools
32
32
 
33
33
  - None beyond Claude Code itself
34
- - Playwright CLI optional for verifying form submissions and interactive surfaces. See [`claude-ui-test`](../claude/skills/claude-ui-test/SKILL.md).
34
+ - Playwright CLI optional for verifying form submissions and interactive surfaces. See [`claude-ui-test`](../../claude/skills/claude-ui-test/SKILL.md).
35
35
 
36
36
  ### Skills
37
37
 
@@ -66,16 +66,16 @@ Impeccable, if installed, keeps its own root `DESIGN.md` and `PRODUCT.md` in the
66
66
 
67
67
  ### Tools
68
68
 
69
- - Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../wiki/tools/stitch.md).
70
- - Excalidraw canvas server on localhost plus the `yctimlin/mcp_excalidraw` MCP shim, for projects that need an agent to draw, read back, and revise a canvas. See [visual wireframes](../wiki/tools/visual-wireframes.md) for setup and footguns.
71
- - Playwright MCP for browser-side verification. See [Playwright](../wiki/tools/community-mcp-servers.md#playwright-microsoft).
72
- - Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../wiki/tools/community-mcp-servers.md#chrome-devtools-google).
73
- - Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../wiki/claude/claude-design.md).
69
+ - Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../../wiki/tools/stitch.md).
70
+ - Excalidraw canvas server on localhost plus the `yctimlin/mcp_excalidraw` MCP shim, for projects that need an agent to draw, read back, and revise a canvas. See [visual wireframes](../../wiki/tools/visual-wireframes.md) for setup and footguns.
71
+ - Playwright MCP for browser-side verification. See [Playwright](../../wiki/tools/community-mcp-servers.md#playwright-microsoft).
72
+ - Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../../wiki/tools/community-mcp-servers.md#chrome-devtools-google).
73
+ - Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../../wiki/claude/claude-design.md).
74
74
 
75
75
  ### Skills
76
76
 
77
77
  - Everything from tier 0
78
- - A frontend design skill to steer visual quality. Pick one of [Impeccable](../wiki/tools/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../wiki/tools/community-skills.md#nextlevelbuilderui-ux-pro-max-skill), or Anthropic's `frontend-design` plugin. Impeccable is the strongest default because of its curated anti-patterns.
78
+ - A frontend design skill to steer visual quality. Pick one of [Impeccable](../../wiki/tools/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../../wiki/tools/community-skills.md#nextlevelbuilderui-ux-pro-max-skill), or Anthropic's `frontend-design` plugin. Impeccable is the strongest default because of its curated anti-patterns.
79
79
 
80
80
  ### When to pick
81
81
 
@@ -106,9 +106,9 @@ Design happens in a graphical tool. `.claude/DESIGN.md` either regenerates from
106
106
 
107
107
  ### Tools
108
108
 
109
- - Figma desktop app with the [Figma Dev Mode MCP](../wiki/tools/community-skills.md#figma-mcp-and-code-to-canvas) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture.
110
- - Claude Design with its Claude Code handoff bundle for teams without an existing Figma investment and for solo founders or PMs driving design themselves. One-way handoff, no bidirectional sync. See [Claude Design](../wiki/claude/claude-design.md).
111
- - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../wiki/tools/stitch.md).
109
+ - Figma desktop app with the [Figma Dev Mode MCP](../../wiki/tools/community-skills.md#figma-mcp-and-code-to-canvas) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture.
110
+ - Claude Design with its Claude Code handoff bundle for teams without an existing Figma investment and for solo founders or PMs driving design themselves. One-way handoff, no bidirectional sync. See [Claude Design](../../wiki/claude/claude-design.md).
111
+ - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../../wiki/tools/stitch.md).
112
112
  - Playwright and Chrome DevTools MCPs as in tier 1
113
113
 
114
114
  ### Skills
@@ -145,9 +145,9 @@ Resist over-tiering early. Moving up is cheap because tiers stack. Moving down m
145
145
 
146
146
  ## References
147
147
 
148
- - [Stitch](../wiki/tools/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
149
- - [Claude Design](../wiki/claude/claude-design.md): first-party hosted design product and handoff bundle
148
+ - [Stitch](../../wiki/tools/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
149
+ - [Claude Design](../../wiki/claude/claude-design.md): first-party hosted design product and handoff bundle
150
150
  - `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
151
- - [Visual wireframes](../wiki/tools/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
152
- - [Community skills and plugins](../wiki/tools/community-skills.md): catalog of frontend design skills and integrations
153
- - [Community MCP servers](../wiki/tools/community-mcp-servers.md): catalog of MCPs referenced across all tiers
151
+ - [Visual wireframes](../../wiki/tools/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
152
+ - [Community skills and plugins](../../wiki/tools/community-skills.md): catalog of frontend design skills and integrations
153
+ - [Community MCP servers](../../wiki/tools/community-mcp-servers.md): catalog of MCPs referenced across all tiers
@@ -38,6 +38,8 @@ The block sits after any `PATH` mutations and the `claude` CLI install. Zsh expa
38
38
 
39
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.
40
40
 
41
+ The block itself works unchanged in bash, and what differs is the file it goes in and who writes it. Paste it into `~/.bashrc` by hand, since `bun run bootstrap` appends to `~/.zshrc` alone and a run from a bash shell installs nothing that shell reads. Bash re-checks the first word of an alias expansion the way zsh does, so the `cl` and `clp` chains below still inherit their base, and the `bun install` and `bun link` step above is the opt-out either shell takes.
42
+
41
43
  ## What each one does
42
44
 
43
45
  `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.
@@ -66,7 +68,7 @@ Use `clp` in any other repository where you want the toolkit skills available. W
66
68
 
67
69
  Use `cls` or `clps` to save Opus usage on routine sessions. Switch mid-session with `/model` to avoid restarting.
68
70
 
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.
71
+ 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.
70
72
 
71
73
  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
74
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.21.0",
4
+ "version": "4.22.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -42,7 +42,8 @@ is_internal_topic() {
42
42
  # sorted so a domain split into a folder lands in its alphabetical place rather
43
43
  # than after every file. A folder declares its category on its own index, since
44
44
  # the allowlist is what separates a target-facing doc from a workflow one and a
45
- # split domain is not exempt from it.
45
+ # split domain is not exempt from it. A sub-area file declares its own, which is
46
+ # what keeps a page's listing membership unchanged by the folder it moves into.
46
47
  collect_docs() {
47
48
  local file name description category
48
49
  {
@@ -64,12 +65,30 @@ collect_docs() {
64
65
  description=$(read_frontmatter_field "$file" "subtitle")
65
66
  printf '%s\t%s\t%s\t%s\n' "$name" "$description" "$category" "docs/$name/index.md"
66
67
  done < <(find "$DOCS_DIR" -mindepth 2 -maxdepth 2 -type f -name "index.md")
68
+
69
+ # A sub-area file is named and described like a sibling file, and reaches a
70
+ # caller by that bare name, so the depth it sits at is not the listing's
71
+ # business. Only a folder carrying an index is walked, matching resolveTopic
72
+ while IFS= read -r file; do
73
+ name=$(basename "$file" .md)
74
+ [ "$name" = "index" ] && continue
75
+ [ -f "$(dirname "$file")/index.md" ] || continue
76
+ category=$(read_frontmatter_field "$file" "category")
77
+ is_target_facing "$category" || continue
78
+ description=$(read_frontmatter_field "$file" "description")
79
+ printf '%s\t%s\t%s\t%s\n' "$name" "$description" "$category" "docs/$(basename "$(dirname "$file")")/$name.md"
80
+ done < <(find "$DOCS_DIR" -mindepth 2 -maxdepth 2 -type f -name "*.md")
67
81
  } | sort
68
82
  }
69
83
 
70
84
  # Emits `name<TAB>description<TAB>target` per context entry, sorted so a domain
71
85
  # split into a folder lands in its alphabetical place rather than after every
72
- # file. Matches listTopics in src/docs/read.ts, which sorts both together.
86
+ # file, the way listTopics in src/docs/read.ts sorts both together.
87
+ #
88
+ # It stops at the folder where listTopics goes on to name each sub-area file.
89
+ # This is the downstream catalog and that one answers what a caller could have
90
+ # typed, so a reachable name absent here is the same divergence the internal
91
+ # topics already carry, and docs/agents/docs.md states it.
73
92
  collect_context() {
74
93
  local file name description
75
94
  {
package/src/docs/read.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
- import { basename, dirname, join } from 'node:path'
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
3
  import { stripFrontmatter } from '@/frontmatter'
4
4
 
5
5
  const INDEX_TOPIC = 'index'
@@ -20,6 +20,11 @@ export interface ResolvedTopic {
20
20
  * A domain too large for one file splits into `<domain>/` with a generated
21
21
  * `index.md`, so a topic names either a sibling file or such a folder. Both
22
22
  * spellings resolve to the same name a caller types.
23
+ *
24
+ * A sub-area file one level down resolves by its bare name too, but only after
25
+ * every root has been tried for both spellings above. Reading the folder last
26
+ * is what keeps the widening additive: no name that resolved before this
27
+ * resolves anywhere else now.
23
28
  */
24
29
  export function resolveTopic(
25
30
  root: string,
@@ -37,39 +42,97 @@ export function resolveTopic(
37
42
  }
38
43
  }
39
44
 
40
- return undefined
45
+ const leaves = findLeaves(root, topic)
46
+ if (leaves.length !== 1) return undefined
47
+
48
+ const rel = leaves[0] as string
49
+ return { path: join(root, rel), rel }
50
+ }
51
+
52
+ /**
53
+ * Names every sub-area file called `<topic>.md`, across both roots. A name
54
+ * carried by more than one folder resolves to none of them, since answering a
55
+ * bare `overview` with whichever folder sorts first is a confident wrong answer
56
+ * where the miss is a listing of what the caller could have typed.
57
+ */
58
+ function findLeaves(root: string, topic: string): string[] {
59
+ const matches: string[] = []
60
+
61
+ for (const dir of ROOTS) {
62
+ for (const folder of catalogedFolders(join(root, dir))) {
63
+ const rel = join(dir, folder, `${topic}.md`)
64
+ if (existsSync(join(root, rel))) matches.push(rel)
65
+ }
66
+ }
67
+
68
+ return matches
69
+ }
70
+
71
+ /**
72
+ * The split folders of one root, which is every sub-folder carrying an index.
73
+ * A folder without one is absent from both verbs, so its files stay out of
74
+ * reach with it rather than becoming nameable through a catalog nobody wrote.
75
+ */
76
+ function catalogedFolders(dir: string): string[] {
77
+ if (!existsSync(dir)) return []
78
+
79
+ return readdirSync(dir, { withFileTypes: true })
80
+ .filter((entry) => entry.isDirectory())
81
+ .map((entry) => entry.name)
82
+ .filter((name) => existsSync(join(dir, name, `${INDEX_TOPIC}.md`)))
83
+ .sort()
41
84
  }
42
85
 
43
86
  /**
44
87
  * Names every topic a `get` could resolve, listed per root in the order the
45
88
  * roots are searched. Shown when a topic misses, so it doubles as the answer to
46
- * what the caller should have typed.
89
+ * what the caller should have typed, which is why it carries exactly what
90
+ * `resolveTopic` answers for and nothing else.
47
91
  */
48
92
  export function listTopics(root: string): string[] {
49
- const topics: string[] = []
50
-
51
- for (const dir of ROOTS) {
52
- const cwd = join(root, dir)
53
- if (!existsSync(cwd)) continue
93
+ const named = ROOTS.map((dir) => collectNamed(root, dir))
94
+ const leaves = ROOTS.map((dir) => collectLeaves(root, dir))
54
95
 
55
- const files = [
56
- ...new Bun.Glob('*.md').scanSync({ cwd, onlyFiles: true, dot: true }),
57
- ]
58
- .map((name) => basename(name, '.md'))
59
- .filter((name) => name !== INDEX_TOPIC)
60
-
61
- const folders = [
62
- ...new Bun.Glob(`*/${INDEX_TOPIC}.md`).scanSync({
63
- cwd,
64
- onlyFiles: true,
65
- dot: true,
66
- }),
67
- ].map((name) => dirname(name))
68
-
69
- topics.push(...[...files, ...folders].sort())
96
+ const taken = new Set(named.flat())
97
+ const counts = new Map<string, number>()
98
+ for (const name of leaves.flat()) {
99
+ counts.set(name, (counts.get(name) ?? 0) + 1)
70
100
  }
71
101
 
72
- return topics
102
+ const reachable = (name: string): boolean =>
103
+ !taken.has(name) && counts.get(name) === 1
104
+
105
+ return ROOTS.flatMap((_, position) =>
106
+ [
107
+ ...(named[position] as string[]),
108
+ ...(leaves[position] as string[]).filter(reachable),
109
+ ].sort(),
110
+ )
111
+ }
112
+
113
+ /** The sibling files and split folders of one root, which shadow every leaf. */
114
+ function collectNamed(root: string, dir: string): string[] {
115
+ const cwd = join(root, dir)
116
+ if (!existsSync(cwd)) return []
117
+
118
+ return [...markdownNames(cwd), ...catalogedFolders(cwd)]
119
+ }
120
+
121
+ /** One entry per occurrence, so a name carried twice is counted twice. */
122
+ function collectLeaves(root: string, dir: string): string[] {
123
+ const cwd = join(root, dir)
124
+
125
+ return catalogedFolders(cwd).flatMap((folder) =>
126
+ markdownNames(join(cwd, folder)),
127
+ )
128
+ }
129
+
130
+ function markdownNames(dir: string): string[] {
131
+ return [
132
+ ...new Bun.Glob('*.md').scanSync({ cwd: dir, onlyFiles: true, dot: true }),
133
+ ]
134
+ .map((name) => basename(name, '.md'))
135
+ .filter((name) => name !== INDEX_TOPIC)
73
136
  }
74
137
 
75
138
  export function readTopic(topic: ResolvedTopic): string {