@erclx/canon 4.58.0 → 4.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.58.0",
4
+ "version": "4.60.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -22,6 +22,7 @@ A track that closes with several separable findings writes only one task, and th
22
22
  - Carry a lean and the finding that would overturn it on every open question, or admit that a measurement is missing
23
23
  - Confine writes to the track folder, with the close-time task file, the experiment fixture, and the intake routing below as the only exceptions
24
24
  - Route a closing-track finding the required task does not cover through `claude-intake`, rather than leaving it to be asked about. The route runs in the same session, so it is a write outside the folder rather than a handoff to a later one.
25
+ - Place the closing task's row through `claude-tasks` Step 4 rather than writing `priority.md` or `backlog.md` directly
25
26
  - Link every claim about a source outside the project, and list an unread source as a lead rather than citing it
26
27
  - Put a fixture a headless run is pointed at outside the repository
27
28
  - Write the next-session file self-contained, since the folder is unbacked and dies with the machine
@@ -92,7 +92,7 @@ The standard sets the open question format and requires it inside a topic file a
92
92
  1. Write `06-decision.md`. It states the problem once, names the goal, lists what to do, and lists what was considered and dropped.
93
93
  2. Write `07-next-session.md` self-contained, so it survives a compaction that loses the conversation.
94
94
  3. Update the file map in `README.md`.
95
- 4. Write one task file in `.canon/tasks/` recording what the track concluded, even when the conclusion is to do nothing. Follow `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` for the filename and frontmatter. Aside from an experiment fixture, this and the routing in Step 5 are the only ways close mode reaches outside the folder.
95
+ 4. Write one task file in `.canon/tasks/` recording what the track concluded, even when the conclusion is to do nothing. Follow `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` for the filename and frontmatter. Place the row through `claude-tasks` Step 4, which checks the roster for a live orchestrator before writing `priority.md` or `backlog.md` directly. Aside from an experiment fixture, this and the routing in Step 5 are the only ways close mode reaches outside the folder.
96
96
  5. When the task written in Step 4 does not cover every finding the track surfaced, route what it leaves out through `claude-intake`. Skip this step when it does.
97
97
  6. Report uncited external claims. Closing already reads every file in the folder, so list any statement about a source outside the project that carries neither a link nor a lead entry. Report and do not block, because judging whether a sentence makes an external claim is the call a checker gets wrong.
98
98
 
@@ -65,7 +65,7 @@ For each in-scope entry (see Scope), pick one action:
65
65
  - **Promote to `CLAUDE.md`**: the rule is cross-domain behavior or a design principle applied across the whole project.
66
66
  - **Promote to a skill body**: the rule fires only when editing a specific path-scoped domain. Name the target skill.
67
67
  - **Promote to a standards file**: the rule is an authoring reference that belongs in the project's own standards folder as `<domain>.md`.
68
- - **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `.claude/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` in the format `claude-memory-capture` writes, and tell the user to run `/claude-docs` from a branch. Do not edit the context entry here.
68
+ - **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `.claude/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, in the format `claude-memory-capture` writes, and tell the user to run `/claude-docs` from a branch. Do not edit the context entry here.
69
69
  - **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning). Do not author the rule file inline. Never edit the synced `.claude/rules/` copies of toolkit rules, because `canon gov sync` overwrites them. Stop at handoff.
70
70
  - In the toolkit repo, point the user at `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, which own the source-of-truth rules under `governance/rules/`.
71
71
  - In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`.
@@ -150,7 +150,7 @@ Free-form text after the verb is a reason. Capture it in the receipt but do not
150
150
  Action by action type:
151
151
 
152
152
  - **Promote**: use `Edit` to insert the rewritten rule into the target surface, then archive the memory file.
153
- - **Promote to a context entry**: append the fact to `.canon/tmp/memory-routing/<slug>.md`, then archive the memory file. `claude-docs` folds it in on its next run from a branch, which is what keeps one skill writing context entries.
153
+ - **Promote to a context entry**: append the fact to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, then archive the memory file. `claude-docs` folds it in on its next run from a branch, which is what keeps one skill writing context entries.
154
154
  - **Hand off**: do not edit governance. Archive the memory file only if the user confirmed the handoff explicitly. Otherwise leave it in place.
155
155
  - **Retire**: archive the memory file.
156
156
 
@@ -141,7 +141,7 @@ Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor
141
141
 
142
142
  ## Step 4: post to the PR
143
143
 
144
- Write the comment to `.canon/tmp/pr-review/body-<number>-<short-sha>.md`, which the rest of this step calls `<body-file>`. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post. The head commit stops a second pass overwriting the first one's body, and leaves the folder a record of which commit each review covered.
144
+ Write the comment to `.canon/tmp/pr-review/body-<number>-<short-sha>.md` at the main worktree root, not the current worktree, which the rest of this step calls `<body-file>`. Resolve that root the way `claude-worktree` does, and send the write as a plain single `Bash` command carrying a heredoc from a linked worktree, since `Edit` and `Write` refuse a main-root path there. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post, and the head commit stops a second pass overwriting the first one's body, leaving the folder a record of which commit each review covered.
145
145
 
146
146
  Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
147
147
 
@@ -11,12 +11,15 @@ Without this skill, a task file gets a filename and frontmatter invented on the
11
11
 
12
12
  Archiving fails in two ways that both lose work. Moving the file, editing the ordering file, and regenerating the index as three separate acts drifts from the one command the merge hook calls, so the attended and unattended paths stop agreeing. And an all-`[x]` task gets archived while its pull request is still open, because marking outcomes happens on the branch as the first step of shipping. The board is gitignored, so nothing restores a task archived early.
13
13
 
14
+ Placing a row without checking for another writer collides the same way. Two sessions filing work at once can claim one phase label twice or land two rows beside each other unread, since neither reads the board before writing it.
15
+
14
16
  ## Must
15
17
 
16
18
  - Resolve the board at the main worktree root, since a linked worktree writing to `pwd` creates a second board nothing reads
17
19
  - Read the tasks standard before writing, rather than working the filename and frontmatter from memory
18
20
  - Require an origin at creation, because that is the only moment the invariant is enforceable
19
21
  - Propose the phase label from what is already on the board and pad it to two digits
22
+ - Check the roster for a live orchestrator before writing a row, and hand off rather than write when one is found
20
23
  - Confirm the work reached the default branch before archiving
21
24
  - Run the archive command and route on the reason it refuses, since each reason has one resolution
22
25
  - Report an origin that carries no task, as a list rather than a prompt
@@ -28,6 +31,7 @@ Archiving fails in two ways that both lose work. Moving the file, editing the or
28
31
  - Write a pull request line at creation, when any number is a guess at someone else's work
29
32
  - Archive around a refusal
30
33
  - Split the legacy single-file archive into per-task files nobody wrote
34
+ - Fall through to a direct write when no orchestrator is found and this session's own name starts with `worker-` or `planner-`, since those role bodies ban the write with no exception
31
35
 
32
36
  ## Guards
33
37
 
@@ -47,9 +47,15 @@ Write it immediately. Claude Code's tool permission dialog is the confirmation g
47
47
 
48
48
  A task file with no row is a dropped task, so name the surface it lands on in the same pass that creates it. A task that would plausibly be planned within the next few waves takes a row under `## Needs a plan` in `.canon/tasks/priority.md`, positioned by where it sits against the rows already there, with the reason for that position in its `Waiting on` cell. Anything else takes a line in `.canon/tasks/backlog.md`, which is unordered and where in the file it goes means nothing.
49
49
 
50
- The test and both file shapes are in `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`. From a linked worktree the file-editing tools refuse the main root, so a row lands through the same `Bash` route the file itself took.
50
+ The test and both file shapes are in `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`.
51
51
 
52
- Say which surface it went to and why in the report. The call is a judgment restated on every sweep rather than a property of the task, and a placement with no stated reason is one the next sweep re-derives from nothing.
52
+ Check the roster for a live orchestrator before writing either file. Read `canon sessions list --self --json` for this session's own `sessionId` and `name`, then read `canon sessions list --json` and match a row whose `repository` matches this session's own, whose `sessionId` differs from it, and whose `name` starts with `orchestrator-`. That is the convention every hand-launched controller on this machine currently uses, and it excludes every `worker-` and `planner-` session cleanly. Treat a refusal from either call the same as a roster read that failed.
53
+
54
+ - **Found.** Do not write `priority.md` or `backlog.md`. Report the surface the row would take and the reason worked out above, then hand off by messaging that session with the same information so it places the row itself rather than two sessions writing the board at once.
55
+ - **Not found, and this session's own name (already read above) starts with `worker-` or `planner-`.** Do not write either file. Those two role bodies ban a board write with no exception, and an orchestrator absent from the roster is not consent to break it. Report the row and the surface it would take, so the operator or a later orchestrator places it.
56
+ - **Not found otherwise, or the roster read fails.** Write the row directly, as today. A failed read is indistinguishable from a solo project holding no orchestrator, and stopping would strand the row on the one path this section otherwise keeps unconditional. From a linked worktree the file-editing tools refuse the main root, so the row lands through the same `Bash` route the file itself took.
57
+
58
+ Say which branch fired and why in the report. The call is a judgment restated on every sweep rather than a property of the task, and a placement with no stated reason is one the next sweep re-derives from nothing.
53
59
 
54
60
  ### Step 5: report unlinked origins
55
61
 
@@ -9,7 +9,7 @@ description: Scope boundary for the one-shot onboarding chain and the setup step
9
9
 
10
10
  Without this skill, a session installs a stack whose name it recalled rather than read, hand-writes configs the tooling sync already ships as golden files, and re-runs the chain over a project that owns its configs already.
11
11
 
12
- Two failures are the expensive ones because both return success. A monorepo run drops husky into every subtree, git honors one `core.hooksPath`, and the extra hook directories go quiet with nothing reporting it. A caller reads the chain as onboarding complete when index bootstrap and plugin provisioning never ran, since no step states whether they belong to the chain.
12
+ Two failures are the expensive ones because both return success. A monorepo run drops husky into every subtree, git honors one `core.hooksPath`, and the extra hook directories go quiet with nothing reporting it. A caller reads the chain as onboarding complete when plugin provisioning never runs, since no step states that it sits outside the chain.
13
13
 
14
14
  Two more start at the resolve step. A project whose language the toolkit carries no stack for resolves to `base`, and the preview reports the resolved name without marking it as a fallback, so `base` development dependencies, scripts, and hooks land on a project that will not use them and nobody had the moment to decline.
15
15
 
@@ -20,7 +20,7 @@ The other is the refusal that ends rather than routes. An existing project, an i
20
20
  - Resolve every domain argument against a catalog read at run time, so a stack, rule, snippet, or tooling name the catalog does not carry cannot reach the command line
21
21
  - Preview the resolved chain before the first command runs, naming the evidence file behind each detected technology
22
22
  - Pass `--skip base` on every subtree past the first in a monorepo, so the shared hook layer lands once
23
- - State which onboarding steps the chain does not run, so a caller knows what is left rather than inferring completeness from a clean report
23
+ - State which onboarding steps the chain does not run and why, so a caller knows what is left and where the boundary sits rather than inferring completeness from a clean report
24
24
  - Mark a stack reached by fallback as a fallback in the preview, and name what `base` carries, so the write can be declined at the one point it is still declinable
25
25
  - Name a destination for every state the chain does not serve, in this body rather than only in a reference doc the person would have to already know to open, and say for each whether the chain stops or runs on a default
26
26
 
@@ -37,9 +37,10 @@ The other is the refusal that ends rather than routes. An existing project, an i
37
37
  ## Out of scope
38
38
 
39
39
  - Installing governance rules alone: `setup-gov`. This skill installs governance beside tooling and the seeds.
40
- - Bootstrapping the `index.md` system: `setup-indexes`. Neither `canon init` nor this chain performs it.
41
40
  - Provisioning Claude Code plugins: `setup-plugins`. Those install once per machine rather than into a project, so no project-scoped chain can carry them.
42
- - Running the verification scripts, which `setup-verify` owns and this chain invokes as its last step
41
+ - Running the verification scripts, which `setup-verify` owns and this chain invokes as step 4
42
+ - Publishing repository metadata: `repo-metadata`. It reaches a remote, which is past the project edge this chain stops at.
43
+ - Making the first commit: `git-commit`. It reaches the project's history, which is past the project edge this chain stops at.
43
44
  - Re-installing into a project that already has the files, which the per-domain `canon <domain> install` and `canon sync` commands own
44
45
  - Deciding which of those per-domain commands an existing project needs: `canon-operator`. That skill reads the target before naming one, which this chain never does, so it is the destination rather than a guess made here.
45
46
  - Seeding the `.claude/` folder without the tooling chain, which `canon claude init` does on its own and `setup-indexes` finishes
@@ -10,7 +10,7 @@ Orchestrates the onboarding chain. Detects project type, resolves per-domain arg
10
10
  ## Scope
11
11
 
12
12
  - This skill and `canon init` run once on a fresh scaffold, never on an existing project. They do not guard against clobbering existing configs. When tempted to add guards, mode switches, or an existing-project branch, stop. Extend the per-domain `canon <domain> install` or `canon sync` paths instead.
13
- - The chain does not bootstrap the `index.md` system and does not provision Claude Code plugins. `setup-indexes` owns the first. `setup-plugins` owns the second, which installs once per machine rather than into a project, so no project-scoped chain can carry it. Name both in the report so a clean result does not read as onboarding complete.
13
+ - The chain folds `setup-indexes` in as its own final step, once the project-scoped work is written. It still does not provision Claude Code plugins: `setup-plugins` owns that, installing once per machine rather than into a project, so no project-scoped chain can carry it. Name it in the report so a clean result does not read as onboarding complete.
14
14
 
15
15
  ## Declined states
16
16
 
@@ -75,7 +75,7 @@ A resolved name and a fallback read alike once written, so mark the fallback her
75
75
 
76
76
  ## Execute
77
77
 
78
- Run the chain in order, starting immediately after the preview. Each step's permission dialog is the confirmation gate. Do not pause for additional confirmation. Run from the target project's current directory.
78
+ Run the chain in order, starting immediately after the preview. Each step's permission dialog is the confirmation gate, except step 5, which hands off to a skill that confirms its own folder list with the operator. Do not pause for additional confirmation elsewhere. Run from the target project's current directory.
79
79
 
80
80
  Step 1: `canon init` installs base tooling, claude seeds, governance rules, and wiki.
81
81
 
@@ -115,6 +115,8 @@ Do not generate ESLint, Vitest, or Playwright configs. They ship as golden files
115
115
 
116
116
  Step 4: invoke `setup-verify`. Runs the `package.json` scripts and reports pass/fail.
117
117
 
118
+ Step 5: hand off to `setup-indexes` to bootstrap the `index.md` system over the project's own documentation folders. The skill confirms candidate folders with the operator rather than running unattended, which is the one step in this chain that pauses for a conversation. A fresh scaffold usually has no markdown-heavy folder yet, so this step frequently hands off with nothing for `setup-indexes` to present, which is that skill's own outcome to define rather than a claim this chain makes for it.
119
+
118
120
  ## Report
119
121
 
120
122
  After the chain, report:
@@ -123,6 +125,9 @@ After the chain, report:
123
125
  - Tooling stack synced (or skipped). Name the layers pulled via the extends chain.
124
126
  - Any post-sync fixups applied (ESLint pin, filename renames)
125
127
  - `setup-verify` outcome
128
+ - `setup-indexes` outcome (folders bootstrapped, or none found)
126
129
  - Any domains or scripts that failed
127
130
  - Any detection gaps surfaced during resolve
128
- - Onboarding steps left to the caller: `setup-indexes` for the `index.md` system, `setup-plugins` for Claude Code plugins
131
+ - Onboarding steps left to the caller: `setup-plugins` for Claude Code plugins
132
+
133
+ The chain stops at the project edge. `repo-metadata` and `git-commit` also ship, reaching a remote and the project's history respectively, and neither runs here.
@@ -214,7 +214,7 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
214
214
  | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
215
215
  | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
216
216
  | Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line |
217
- | Mapping | A row or backlog line naming no task file, and a task file neither surface names |
217
+ | Mapping | A row or backlog line naming no task file |
218
218
  | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
219
219
  | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
220
220
  | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
@@ -224,9 +224,9 @@ Shape runs before any other check reads a row, since a row failing it carries no
224
224
 
225
225
  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.
226
226
 
227
- 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.
227
+ Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A row or a backlog line naming no task file is `task-unresolved`, and a file both surfaces name is `row-duplicated` for the reason a task in two groups is: it claims two things about itself and only one can hold. A task file neither surface names is `unplaced` rather than a finding, since that is the normal state between a session filing it and a live orchestrator placing it on the board. One check across both is what lets a task move between them without the move reading as a dropped file.
228
228
 
229
- 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.
229
+ 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 still lands in the `unplaced` array below rather than the findings. 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.
230
230
 
231
231
  Ordering reads a `## Needs a plan` row's `Waiting on` cell for the position it claims, and reports two failures off one walk. A row stating an ordinal is checked against where it actually sits, which is `row-misordered`. A row claiming no position in either form it may take is `row-unranked`, since its cell argues the task matters and ranks it against nothing, which leaves the order recording when each row was filed.
232
232
 
@@ -282,6 +282,21 @@ That claim collides with every row a later session writes under the folder, and
282
282
 
283
283
  The scan reaches `## Run now` and stops, where the collision check stops. A cell in another group describes work nobody has planned, written as a sentence and rewritten once a plan exists, so a claim read off one reports on prose rather than on a file set. A parked folder claim surfaces when its row is promoted, which is when the cell becomes something a dispatcher can act on.
284
284
 
285
+ A task file neither surface names lands in a fourth array, on the same reasoning:
286
+
287
+ ```json
288
+ {
289
+ "unplaced": [
290
+ {
291
+ "subject": "v50.6-a-standard-no-skill-reads", // canon-allow-reference: shows the subject field's real vXX.Y-slug shape, not a citation of a real task
292
+ "message": "is a task file with no row on the board and no line on the backlog."
293
+ }
294
+ ]
295
+ }
296
+ ```
297
+
298
+ Under the roster-checked hand-off `claude-tasks` and `claude-groundwork` state, filing a task and placing its row are two acts a different session each may take, so a task caught between the two is ordinary rather than a finding and this array moves no exit code. It still reports, since it is the only local detector for a row a hand-edit dropped, a handoff message that never arrived, or an orchestrator that ended before placing it.
299
+
285
300
  Exit codes: `0` every check passed, `1` refused, `2` at least one finding. The `reason` field carries which gate refused: `no-board`, `no-ordering`, or `no-groups`. A board grouping under headings of its own trips `no-groups` rather than being read against columns it never declared.
286
301
 
287
302
  Columns are read from each table's own header rather than by position, so a project whose board differs from this one is reported for what it lacks. The `index`, `priority`, and `backlog` siblings are skipped, along with every pre-compaction handoff, which takes one file per session under a `session-` prefix. None of them is a task, and a handoff counted as one would be reported as a task carrying no row on every session that wrote one.
@@ -59,8 +59,9 @@ The chain is:
59
59
  - `canon tooling sync <stack> --write` adds stack-specific deps, scripts, and gitignore entries
60
60
  - The agent reads `canon tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations: .claude/context/development.md -->
61
61
  - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
62
+ - `setup-indexes` bootstraps the `index.md` system over the project's own documentation folders, confirming candidate folders with the operator rather than running unattended
62
63
 
63
- The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `canon:canon-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `canon claude init` for the seed docs and then `canon:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
64
+ The chain stops at the project edge. `repo-metadata` and `git-commit` also ship, reaching a remote and the project's history respectively, and neither runs as part of it. The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `canon:canon-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `canon claude init` for the seed docs and then `canon:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
64
65
 
65
66
  Keep the `## Scripts` table in `.claude/context/development.md` current as scripts are added. Base tooling seeds that entry with the commands it installs, and each stack reference extends the table. `project-commands` reads it to start the app or run a check on request, so a command missing from the table cannot be run that way. A project whose entry outgrew one file and split into `.claude/context/development/` keeps the table in `overview.md`, which is where the skill looks next. <!-- audit-ignore-citations: .claude/context/development.md -->
66
67
 
@@ -8,3 +8,4 @@ description: Write temporary files to the scratch folder structure
8
8
 
9
9
  - Write temporary files to `.canon/tmp/<slug>/<file>.md` in the project root, a nested `<slug>/` folder with a kebab-slug tied to the topic, not a flat `<slug>-<file>.md`. The scratch-guard hook enforces the location.
10
10
  - Write to `.claude/.tmp/<slug>/` instead in a project that carries no `.canon/` root, which is one the record move has not reached. The hook accepts either, but the ignore file is the source: `.gitignore` and its manifest counterpart are what keep the fallback spelling out of `git status`, and the hook and this rule both defer to it. `canon migrate records` is what moves a project from this second spelling to the first. <!-- canon-keep-record-root -->
11
+ - "The project root" above means the current worktree by default. `085-worktrees.md` states the same mixed default for `.canon/tmp/` and defers to each skill's own body for which write resolves at the main root instead.
@@ -11,6 +11,7 @@ description: Route tracked-file writes and shared session scratch correctly from
11
11
  ## Shared session scratch
12
12
 
13
13
  - Shared session scratch (`.canon/plans/`, `.canon/review/`, `.canon/memory/`, `.canon/tasks/`) lives at the main worktree root, not inside a linked worktree. From a linked worktree, resolve these paths against the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`. Fall back to `pwd` if not a git repo.
14
+ - `.canon/tmp/` is mixed rather than main-root by default: it stays worktree-local per `055-scratch.md`, and a skill that resolves a write there at the main root instead states so in its own body.
14
15
  - From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`.
15
16
  - From a linked worktree, `Edit` and `Write` are refused for every main-root path, session scratch included. The refusal names session isolation and points at the worktree copy, which is a second gitignored file no later session reads, so never take that redirect.
16
17
  - `Read` resolves against the main root normally from a linked worktree. A main-root write reaches it only through `Bash`, as one plain command rather than a compound one, which is refused for complexity.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.58.0",
4
+ "version": "4.60.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -155,9 +155,9 @@ export function register(program: Command): void {
155
155
  "It also grades the title alone against standards/pr.md's ## Title",
156
156
  'section: the `<type>(<scope>): <subject>` structure, lowercase casing',
157
157
  'for the type, the scope, and the first subject word, and a 72-',
158
- 'character length cap. A review carries no title of its own, so this',
159
- 'check is skipped there rather than graded against the forced empty',
160
- 'string.',
158
+ 'character length cap. A scan given no title, whether from a review',
159
+ 'or from a bare --body or --body-file invocation, skips this check',
160
+ 'rather than grading an empty string.',
161
161
  '',
162
162
  'Exit codes:',
163
163
  ' 0 none of the five found',
@@ -287,12 +287,10 @@ async function runScan(opts: ScanOptions): Promise<number> {
287
287
 
288
288
  const result = scanPhaseLabels(resolved)
289
289
  const spelling = await scanTitleSpelling(resolved.title, process.cwd())
290
- // A review carries no title of its own, so `resolved.title` is forced
291
- // empty and grading it would fail as `structure` for the wrong reason.
290
+ // A scan given no title, whatever produced that absence, has nothing to
291
+ // grade and would otherwise fail as `structure` for the wrong reason.
292
292
  const titleFormat =
293
- resolved.source === 'pull-request'
294
- ? checkTitleFormat(resolved.title)
295
- : undefined
293
+ resolved.title !== '' ? checkTitleFormat(resolved.title) : undefined
296
294
 
297
295
  logStep(resolved.source === 'review' ? 'Review comment' : 'Pull request')
298
296
  logInfo(
@@ -373,7 +371,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
373
371
  : 'Title format issue found',
374
372
  )
375
373
  if (titleFormat === undefined) {
376
- logInfo('a review comment carries no title, so there is no format to grade')
374
+ logInfo('the scan carries no title, so there is no format to grade')
377
375
  } else if (titleFormat.conforms) {
378
376
  logInfo(
379
377
  'the title matches <type>(<scope>): <subject> and its casing and length rules',
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  type Finding,
24
24
  type FolderClaim,
25
+ type Unplaced,
25
26
  type Untested,
26
27
  type ValidateOutcome,
27
28
  validateBoard,
@@ -887,6 +888,17 @@ function reportValidation(
887
888
  } else {
888
889
  for (const claim of outcome.claims) logWarn(describeClaim(claim))
889
890
  }
891
+
892
+ // Unplaced is the normal state between a session filing a task and an
893
+ // orchestrator placing its row, so it moves no exit code. It still reports,
894
+ // since it is the only local detector for a row a hand-edit dropped or a
895
+ // handoff that never arrived.
896
+ logStep('Unplaced')
897
+ if (outcome.unplaced.length === 0) {
898
+ logInfo('every task file sits on the board or the backlog')
899
+ } else {
900
+ for (const task of outcome.unplaced) logWarn(describeUnplaced(task))
901
+ }
890
902
  outro()
891
903
  }
892
904
 
@@ -901,6 +913,7 @@ function reportValidation(
901
913
  findings: outcome.findings,
902
914
  untested: outcome.untested,
903
915
  claims: outcome.claims,
916
+ unplaced: outcome.unplaced,
904
917
  })}\n`,
905
918
  )
906
919
  }
@@ -921,6 +934,10 @@ function describeClaim(claim: FolderClaim): string {
921
934
  return `${claim.group}: ${claim.subject} ${claim.message}`
922
935
  }
923
936
 
937
+ function describeUnplaced(task: Unplaced): string {
938
+ return `${task.subject} ${task.message}`
939
+ }
940
+
924
941
  async function runArchive(
925
942
  task: string | undefined,
926
943
  opts: ArchiveCommandOptions,
@@ -38,7 +38,6 @@ export const FINDING_KINDS = [
38
38
  'plan-uncited',
39
39
  'plan-mismatched',
40
40
  'task-unresolved',
41
- 'row-missing',
42
41
  'row-duplicated',
43
42
  'row-misshapen',
44
43
  'row-untabled',
@@ -83,6 +82,18 @@ export interface FolderClaim {
83
82
  readonly message: string
84
83
  }
85
84
 
85
+ /**
86
+ * A task file neither surface names. That is the normal state between a
87
+ * session filing it and a live orchestrator placing it, so it reports beside
88
+ * the findings and moves no exit code. It is still the only local detector for
89
+ * a row lost to a hand-edit, a handoff message that never arrived, or an
90
+ * orchestrator that ended before placing it.
91
+ */
92
+ export interface Unplaced {
93
+ readonly subject: string
94
+ readonly message: string
95
+ }
96
+
86
97
  /**
87
98
  * A backlog line, which carries a pointer and nothing else. The backlog is
88
99
  * explicitly unordered, so a line has no position to read and no columns to
@@ -116,6 +127,7 @@ export interface ValidateReport {
116
127
  readonly findings: readonly Finding[]
117
128
  readonly untested: readonly Untested[]
118
129
  readonly claims: readonly FolderClaim[]
130
+ readonly unplaced: readonly Unplaced[]
119
131
  }
120
132
 
121
133
  export interface ValidateRefused {
@@ -556,15 +568,15 @@ function resolves(target: string, dir: string, root: string): boolean {
556
568
  }
557
569
 
558
570
  /**
559
- * Accounts every task file against both surfaces the board spans. A task sits
560
- * on `priority.md` when it would plausibly be planned soon and on `backlog.md`
561
- * otherwise, so a file reached by neither is the dropped one this reports and a
562
- * file reached by both claims two contradictory things about itself.
571
+ * Reports a task cited from two places on the board, which claims two
572
+ * contradictory things about itself: a task belongs to exactly one group,
573
+ * board or backlog. A task file neither surface names is no longer reported
574
+ * here, since that is the normal state between a session filing it and a live
575
+ * orchestrator placing it.
563
576
  */
564
577
  function checkMapping(
565
578
  rows: readonly BoardRow[],
566
579
  backlog: readonly BacklogRow[],
567
- stems: readonly string[],
568
580
  dir: string,
569
581
  ): Finding[] {
570
582
  const findings: Finding[] = []
@@ -638,18 +650,6 @@ function checkMapping(
638
650
  }
639
651
  }
640
652
 
641
- for (const stem of stems) {
642
- if (!seen.has(stem) && !listed.has(stem)) {
643
- findings.push({
644
- kind: 'row-missing',
645
- group: undefined,
646
- subject: stem,
647
- message:
648
- 'is a task file with no row on the board and no line on the backlog.',
649
- })
650
- }
651
- }
652
-
653
653
  return findings
654
654
  }
655
655
 
@@ -878,6 +878,32 @@ function checkFolderClaims(
878
878
  return claims
879
879
  }
880
880
 
881
+ /**
882
+ * Reports a task file neither the board nor the backlog names. Under the
883
+ * roster-checked hand-off, filing a task and placing its row are two acts a
884
+ * different session each may perform, so this state is ordinary rather than
885
+ * an error, and it moves no exit code. It still surfaces here rather than
886
+ * nowhere, since it is the only local detector for a row a hand-edit dropped,
887
+ * a handoff message that never arrived, or an orchestrator that ended before
888
+ * placing it.
889
+ */
890
+ function checkUnplaced(
891
+ rows: readonly BoardRow[],
892
+ backlog: readonly BacklogRow[],
893
+ stems: readonly string[],
894
+ ): Unplaced[] {
895
+ const seen = new Set(rows.flatMap((row) => (row.stem ? [row.stem] : [])))
896
+ const listed = new Set(backlog.flatMap((row) => (row.stem ? [row.stem] : [])))
897
+
898
+ return stems
899
+ .filter((stem) => !seen.has(stem) && !listed.has(stem))
900
+ .map((stem) => ({
901
+ subject: stem,
902
+ message:
903
+ 'is a task file with no row on the board and no line on the backlog.',
904
+ }))
905
+ }
906
+
881
907
  /**
882
908
  * Reads the task a blocker cell cites. A task pointer is a bare sibling
883
909
  * filename, the way every `Task` column spells one, so a target carrying a
@@ -1136,7 +1162,7 @@ export async function validateBoard(
1136
1162
 
1137
1163
  const findings = [
1138
1164
  ...shapeFindings,
1139
- ...checkMapping(rows, backlog, stems, dir),
1165
+ ...checkMapping(rows, backlog, dir),
1140
1166
  ...checkPlans(rows, dir, root),
1141
1167
  ...(await checkPlanAgreement(rows, dir, root)),
1142
1168
  ...checkCollisions(rows),
@@ -1152,5 +1178,6 @@ export async function validateBoard(
1152
1178
  findings,
1153
1179
  untested: parked.untested,
1154
1180
  claims: checkFolderClaims(rows, root),
1181
+ unplaced: checkUnplaced(rows, backlog, stems),
1155
1182
  }
1156
1183
  }
@@ -35,4 +35,5 @@ Reference docs for consistent authoring across the toolkit and target projects.
35
35
  - [Tasks reference](tasks.md): Folder layout, filename convention, readiness groups, and content rules for .canon/tasks/
36
36
  - [Teach reference](teach.md): Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
37
37
  - [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
38
+ - [Wiki reference](wiki.md): Folder split, frontmatter, naming convention, and sourcing for a wiki reference page
38
39
  - [Wireframe reference](wireframes.md): Shape and content rules for .claude/wireframes/<surface>.md files
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Wiki reference
3
+ description: Folder split, frontmatter, naming convention, and sourcing for a wiki reference page
4
+ ---
5
+
6
+ # Wiki reference
7
+
8
+ Applies to each authored page under `wiki/`. Skip for `index.md` at any depth, which is a hand-maintained catalog rather than a reference page, and carries `subtitle` rather than `description`.
9
+
10
+ ## Scope
11
+
12
+ Governs each authored page under `wiki/`: which folder it belongs in, its frontmatter, its filename, and how it cites the subject it documents.
13
+
14
+ Does not govern:
15
+
16
+ - Voice, rhythm, sentence construction, and information density: the `write-human` skill
17
+ - Headings, punctuation, word choice, and file references: `markdown.md`
18
+
19
+ ## What a working wiki page looks like
20
+
21
+ A page works when a reader who has never opened it settles two things without asking anyone:
22
+
23
+ - Which folder holds it, decided from the subject alone rather than from where it happened to get written
24
+ - Where the content came from, so a claim can be checked against its owner rather than against this repository
25
+
26
+ A page failing either is non-conforming even when it satisfies every shape rule below.
27
+
28
+ ## Placement
29
+
30
+ - Write a page here only when its subject is owned outside this repository. Route anything about how this repository works to `docs/`, `.claude/context/`, or a skill body instead.
31
+ - File the page by owner: `wiki/claude/` for Anthropic, `wiki/tools/` for another vendor, `wiki/concepts/` where no single vendor owns the subject.
32
+ - Treat `wiki/concepts/rule-writing-vocabulary.md` as the one recorded exception to the owner test, stated in the page itself.
33
+
34
+ ## Frontmatter
35
+
36
+ - `title` (required): sentence case, naming the subject
37
+ - `description` (required): one line naming what the page covers
38
+
39
+ ## Naming
40
+
41
+ - Name a `wiki/claude/` page with a `claude-` prefix before the kebab subject.
42
+ - Name a `wiki/tools/` or `wiki/concepts/` page as a bare kebab subject, with no prefix.
43
+
44
+ ## Sourcing
45
+
46
+ - Close the intro paragraph with a `Source:` sentence naming the owner. Link the canonical page where one exists, and name the owner alone where the subject has no single URL.
47
+ - Fetch current information through the `claude-code-guide` agent when the subject is Claude Code. Do not work from training knowledge.
48
+ - Propose an addition or correction and wait for confirmation. Do not write to a wiki file unasked.
49
+
50
+ ## Template
51
+
52
+ ```markdown
53
+ ---
54
+ title: <Subject>
55
+ description: <one line naming what this page covers>
56
+ ---
57
+
58
+ # <Subject>
59
+
60
+ <What the subject is and why it matters.> Source: <owner, with a link to the canonical page where one exists>.
61
+
62
+ ## <Section>
63
+
64
+ <Reference content.>
65
+ ```
@@ -17,3 +17,6 @@ packages = [
17
17
  [scripts.override]
18
18
  "dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && next dev --port $((3000 + WORKTREE_PORT_OFFSET))"
19
19
  "screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/screenshot.sh"
20
+
21
+ [gitignore]
22
+ "# Next" = [".next/"]
@@ -33,6 +33,12 @@ Next has no config-file port hook, unlike `astro.config.mjs`'s `server.port` or
33
33
 
34
34
  `create-next-app`'s own default `tsconfig.json` needs no changes beyond project-specific path aliases, so a golden copy here would ship nothing the scaffold does not already write.
35
35
 
36
+ ## Gitignore (extend)
37
+
38
+ `[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
39
+
40
+ - `"# Next" = [".next/"]`
41
+
36
42
  ## Development docs (extend)
37
43
 
38
44
  Append to the `## Scripts` table: