@erclx/canon 5.56.0 → 5.57.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.57.0](https://github.com/erclx/canon/compare/v5.56.0...v5.57.0) (2026-10-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * **git-followup:** ship unpushed commits from a clean tree ([#2643](https://github.com/erclx/canon/issues/2643)) ([73bdfbd](https://github.com/erclx/canon/commit/73bdfbdb25ad0a8e76cfbaa57086c8fe058485fe))
9
+ * **plan-feature:** run the existing surface before planning ([#2634](https://github.com/erclx/canon/issues/2634)) ([53c4b93](https://github.com/erclx/canon/commit/53c4b936ed89d42122e251f638e87791278ef883))
10
+ * **pr:** claim root files named in Key changes code spans ([#2639](https://github.com/erclx/canon/issues/2639)) ([6d9e368](https://github.com/erclx/canon/commit/6d9e368d0b6dcda568c3140b1053206fd1038864))
11
+ * **skills:** add run placement and binary rules to planner onboarding ([#2642](https://github.com/erclx/canon/issues/2642)) ([e211beb](https://github.com/erclx/canon/commit/e211bebbc35a709e907fb3dbb0eef75f1d791b42))
12
+ * **skills:** run a repro during intake and triage ([#2638](https://github.com/erclx/canon/issues/2638)) ([82e1fb4](https://github.com/erclx/canon/commit/82e1fb4bdb54eefcea910cf745c80623a8f8368c))
13
+ * **skills:** triage asks plan, ask, or decline ([#2641](https://github.com/erclx/canon/issues/2641)) ([2502248](https://github.com/erclx/canon/commit/2502248fe5ce4ad7dd11a4b27c8f4297ba654fa1))
14
+ * **target-update:** write on a worktree or its own branch ([#2640](https://github.com/erclx/canon/issues/2640)) ([73f2603](https://github.com/erclx/canon/commit/73f2603e034f63397e1b878368aa69d8d4275aaf))
15
+ * **tasks:** resolve closes from the checked-out branch ([#2637](https://github.com/erclx/canon/issues/2637)) ([873834c](https://github.com/erclx/canon/commit/873834c69b8de11efe16bbdaa06187b9328e9738))
16
+
3
17
  ## [5.56.0](https://github.com/erclx/canon/compare/v5.55.0...v5.56.0) (2026-10-23)
4
18
 
5
19
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.56.0",
4
+ "version": "5.57.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -53,10 +53,12 @@ For each remaining issue, measure its claim against the tree during this pass, n
53
53
  - Grep for the construct its defect names and count the sites
54
54
  - Check whether the behavior it asks for exists
55
55
  - Search the log for a commit or pull request that shipped it under another task
56
+ - Run the issue's Repro section in a form that writes nothing when it is one command that works here, and say on `Problem:` what it showed
57
+ - Mark the item `not reproduced here` when the repro names another project's paths or needs more, per `## Repro versus groundwork` in the intake standard
56
58
 
57
59
  Classify each against the toolkit's own surfaces:
58
60
 
59
- - **Direct fix.** One surface, one file, no architectural choice. A typo, a stale reference, a one-line correction.
61
+ - **Direct fix.** A size, not an answer, so it suggests `plan` and `Fix:` says a short plan covers it. One surface, one file, no architectural choice. A typo, a stale reference, a one-line correction.
60
62
  - **Plan-worthy.** Multiple files, a new skill or rule, a behavior change, or a cross-surface move.
61
63
  - **Needs clarification.** The observed and expected behavior conflict or the surface is unnamed. The item asks the operator for the missing detail.
62
64
 
@@ -70,7 +72,7 @@ Write one item per issue in the intake item format, one cluster file per domain
70
72
 
71
73
  - `Problem:` states what this pass measured, with the count, path, or commit behind it
72
74
  - `Fix:` states the class from Step 4 and the change it names
73
- - `Open:` reads `fix, plan, ask, or decline?` on every item
75
+ - `Open:` reads `plan, ask, or decline?` on every item
74
76
  - `Suggested:` opens with exactly one of those tokens, then the reason
75
77
  - `You:` ships empty
76
78
 
@@ -83,14 +85,14 @@ Write `00-overview.md` last, per the intake standard, with the counts by token,
83
85
 
84
86
  **Filed:** <N> issues across <N> clusters, measured against <sha>
85
87
  **Skipped:** <n> already carried
86
- **Suggested:** <n> fix, <n> plan, <n> ask, <n> decline
88
+ **Suggested:** <n> plan, <n> ask, <n> decline
87
89
 
88
90
  Next: /canon:plan-intake-answer to answer the `You:` slots
89
91
  ```
90
92
 
91
- An answered fix or plan item promotes through `task-board` with an `Issue: #NNN` line, and the shipping pull request's `Closes #NNN` drains the queue. Closing a declined issue on GitHub stays the operator's act.
93
+ An answered plan item promotes through `task-board` with an `Issue: #NNN` line, and the shipping pull request's `Closes #NNN` drains the queue. Closing a declined issue on GitHub stays the operator's act.
92
94
 
93
95
  ## Notes
94
96
 
95
97
  - The `feedback` label is what `canon feedback` and the `toolkit-feedback.yml` issue form both apply.
96
- - This skill files and never reimplements. `plan-intake-answer` owns the answers and `task-board` owns the task a fix becomes.
98
+ - This skill files and never reimplements. `plan-intake-answer` owns the answers and `task-board` owns the task a plan item becomes.
@@ -9,19 +9,19 @@ description: What the follow-up push is for, the gaps it closes, and how it spli
9
9
 
10
10
  Without this skill, an edit made after a pull request opens ships as a bare push. The body still describes the scope from before the edit, and a review fix always changes what shipped, so the description a merge records is false by the time review ends. A reviewer who left comments gets no reply, or a second one under the caller's own.
11
11
 
12
- The push itself fails on two branch shapes. A caller that rebased before handing over hits a rejected push, and a branch entered through a worktree carries an open pull request with no tracking ref, which reads as a branch never pushed.
12
+ The push itself fails on two branch shapes. A caller that rebased before handing over hits a rejected push, and a branch entered through a worktree carries an open pull request with no tracking ref, which reads as a branch never pushed. A session that committed in slices arrives with a clean tree and is stopped by a guard that reads only the tree.
13
13
 
14
14
  ## Must
15
15
 
16
- - Every follow-up commit syncs the body when it moves the scope, and the title when the shift makes it inaccurate, whether or not a reply runs
17
- - The message comes from `git-commit`, so a follow-up reads like every other commit
16
+ - Unpushed commits ship like uncommitted ones, with staging and the commit skipped on a clean tree, and a commit this skill makes takes its message from `git-commit`
17
+ - Every follow-up syncs the body when it moves the scope, and the title when the shift makes it inaccurate, whether or not a reply runs
18
18
  - A pull request carrying review comments gets one reply, scanned for banned characters and phase labels and posted through a file, and none when the caller owns it
19
19
  - The push lands on a branch with no tracking ref, and on a rebased one under a lease that protects a commit this session never read
20
20
  - The evidence is posted through `pr-evidence` and read back with `canon pr evidence <number> --check --json`
21
21
 
22
22
  ## Must not
23
23
 
24
- - Run on `main`, or with nothing changed
24
+ - Run on `main`, on a detached head, or with nothing to stage and nothing to push
25
25
  - Open a pull request. A missing one is the stop.
26
26
  - Post a second reply when the caller has already replied
27
27
  - Emit anything after the result line
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: git-followup
3
- description: Ships a small self-review edit on the current PR branch by staging, committing, pushing, and syncing the open PR, replying on the PR when it carries review comments. Use when asked to "ship a followup", "push the PR fix", "followup", or "commit and push this small change". Do NOT use when there is no open PR for the branch (use git-ship instead).
3
+ description: Ships a small self-review edit on the current PR branch by staging, committing, pushing, and syncing the open PR, replying on the PR when it carries review comments. A clean tree with unpushed commits skips the staging and commit. Use when asked to "ship a followup", "push the PR fix", "followup", or "commit and push this small change". Do NOT use when there is no open PR for the branch (use git-ship instead).
4
4
  metadata:
5
5
  family: after-pr
6
6
  ---
@@ -15,7 +15,11 @@ its own reply, so skip the comment in step 7. The push and body sync still run.
15
15
  ## Guards
16
16
 
17
17
  - If `git branch --show-current` returns `main`, stop: `❌ On main. Switch to a PR branch first.`
18
- - If `git status --porcelain` is empty, stop: `❌ No changes to ship.`
18
+ - If `git branch --show-current` is empty, stop: `❌ Detached HEAD. Checkout the PR branch first.` This runs ahead of the next guard, which reads `@{u}`.
19
+ - If `git status --porcelain` is empty and no commit is unpushed, stop: `❌ No changes to ship.`
20
+ - A commit is unpushed when `git rev-list @{u}..HEAD` lists one
21
+ - Count against `origin/<branch>` when `@{u}` does not resolve and that ref does
22
+ - Every commit is unpushed only when neither ref resolves
19
23
  - Resolve `<number>` once, here, through `canon pr head --json`, reading the record's `number`. Every later step names this `<number>` rather than resolving it again.
20
24
  - A record refusing with `ambiguous-pull` stops: `❌ Two open PRs on this branch. Name the one to update.`
21
25
 
@@ -38,6 +42,8 @@ A missing tracking ref is not a guard, since an open pull request proves the bra
38
42
 
39
43
  ## Sequence
40
44
 
45
+ Steps 1 to 3 run only when the tree is dirty. A clean tree carrying unpushed commits already holds its commits, so it starts at step 4.
46
+
41
47
  1. Run `git status` to confirm the changes are intentional
42
48
  2. Run `git add -A` to stage every change
43
49
  3. Invoke `canon:git-commit` to generate one conventional commit from the staged diff
@@ -15,6 +15,14 @@ The plan is the one `task-number.md` names, so reuse the filename an earlier ste
15
15
  canon tasks closes --plan feature-<slug> --json
16
16
  ```
17
17
 
18
+ A branch with no plan runs the verb bare:
19
+
20
+ ```bash
21
+ canon tasks closes --json
22
+ ```
23
+
24
+ The bare call matches the branch against `feat/<slug>` for each task's plan and the names the branch carried before a rename. A planned branch under any other name reads `no-match` and writes no closing line, so pass `--plan` for it.
25
+
18
26
  The verb reads the current branch itself and counts the names it carried before a rename, so pass nothing else. Branch on the record rather than the exit code.
19
27
 
20
28
  | Record | Route |
@@ -13,7 +13,7 @@ A constraint naming a surface to leave alone forbids two acts at once, conformin
13
13
 
14
14
  ## Must
15
15
 
16
- - Read the project's Claude setup before scanning source, and name every file the work touches with the reason it is touched
16
+ - Read the project's Claude setup and the domain's context entry, run the existing thing once read-only, then scan source, and name every file touched with its reason
17
17
  - Reframe a vague ask as observable success criteria, carry each into the plan's verification, and ask when none can be predicted
18
18
  - Slice by user-visible path with the riskiest batch first, and fold a stacked batch sharing most of its files into its parent unless the plan says why they differ
19
19
  - Quote the fetched passage behind each outside source, mark an unfetched one unverified, and place each new path against the layout guidance with a one-clause reason
@@ -23,6 +23,7 @@ A constraint naming a surface to leave alone forbids two acts at once, conformin
23
23
 
24
24
  - Implement, or edit any file the plan describes
25
25
  - Read directories speculatively, since context spent on files the feature does not touch is context the plan does not get
26
+ - Run a form that writes to the tree being planned against, since a planner writes its plan and nothing else
26
27
  - Bundle independent concerns into one plan
27
28
  - Restate the plan in chat during follow-up rounds, since the file is the source of truth
28
29
 
@@ -13,6 +13,13 @@ metadata:
13
13
  - Do not implement anything. Output the plan and stop.
14
14
  - When the feature description spans two or more independent concerns, write one plan file per concern. Do not bundle them under a single slug.
15
15
 
16
+ ## Write scope
17
+
18
+ Write the plan, and nothing else in the tree being planned against.
19
+
20
+ - A file made only to look at goes to scratch under a topic slug, per `${CLAUDE_SKILL_DIR}/references/onboarding.md`
21
+ - A fixture or headless session that must be its own project goes outside the repository, in a temporary directory
22
+
16
23
  ### Predict the next three questions
17
24
 
18
25
  Before Step 3, name the next three questions you would ask about what the ask must achieve, and predict the operator's answer to each. The test reads the ask's success criteria, never the plan's open questions.
@@ -33,7 +40,15 @@ Also read `canon/DESIGN.md` when the feature touches code or UI, for visual inte
33
40
 
34
41
  Coding standards live in `.claude/rules/`. Claude Code loads them automatically, and path-scoped rules apply to the files they match.
35
42
 
36
- ## Step 2: scan relevant source files
43
+ ### Read the domain's context entry
44
+
45
+ Onboard to each domain the feature touches before reading its code, through its context entry. Read `${CLAUDE_SKILL_DIR}/references/onboarding.md` for the lookup.
46
+
47
+ ## Step 2: run the surface, then scan relevant source files
48
+
49
+ When the feature changes a verb, a route, or a rendered surface that already exists, run it once before reading its code and record what it showed. Read `${CLAUDE_SKILL_DIR}/references/onboarding.md` for the safe form per surface kind, the rules that keep the run from writing, and the not-run record.
50
+
51
+ ### Read the code
37
52
 
38
53
  Read the source files directly relevant to the feature, never whole directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
39
54
 
@@ -59,6 +74,7 @@ The section list, what each section holds, and the suggested-and-answer contract
59
74
 
60
75
  What this skill adds on top of the standard:
61
76
 
77
+ - Record each command Step 2 ran and what it showed, or the not-run reason, under `## Current behavior`, and omit the section when nothing ran
62
78
  - Reframe a vague ask as success criteria before `## Files to touch`, each an observable result and its check, which becomes one `## Verification` bullet. "Make it faster" names none.
63
79
  - Confirm a concrete project or use case drives the feature, and lift patterns from it where precedent exists. A feature with no named user is a guess.
64
80
  - Prefer one layout that serves both a new and a grown project over a dual-mode toggle or a migration shim. Two modes double what every later change has to hold.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Onboarding to a domain
3
+ description: The context lookup, the safe run form per surface kind, and the not-run record a planner takes before reading code
4
+ ---
5
+
6
+ # Onboarding to a domain
7
+
8
+ Applies to the first two reads of `plan-feature` Step 1 and Step 2, taken ahead of the source. They go cheapest first, being the context entry, then one run of the existing thing, then the code. A plan starting from the source alone inherits whatever the running code gets wrong.
9
+
10
+ ## Read the context entry
11
+
12
+ - Run `canon indexes list --json canon/context` and read the entries whose title or description matches the domain, rather than walking the folder
13
+ - Report none and continue to the run when the project has no `canon/context/` folder or no entry matches
14
+ - Treat an entry as a claim about the code, to be confirmed by the run
15
+
16
+ ## Run the existing thing once
17
+
18
+ When the feature changes a verb, a route, or a rendered surface that already exists, run it once in a form that writes nothing. Record the command and what it showed.
19
+
20
+ | Surface kind | Safe form |
21
+ | ---------------- | ---------------------------------------------------------------------------- |
22
+ | CLI verb | its read verb, or the `--check` or `--dry-run` form, with `--help` for flags |
23
+ | Route or API | one read request against a running instance |
24
+ | Rendered surface | a capture against a running preview |
25
+ | Anything else | the project's own read command, or a sandbox scenario |
26
+
27
+ Three rules hold across every kind:
28
+
29
+ - Write nothing into the planned tree beyond the plan, so never run a form that writes there. The exceptions are the scratch and outside-repository writes below.
30
+ - Send a verb with no read form to a sandbox scenario or a temporary copy, and name which
31
+ - Skip the step for a feature touching nothing that runs, such as prose or a config seed
32
+
33
+ ## Place the run and its output
34
+
35
+ A checkout is often shared with sessions building other branches, so where a run happens matters as much as what it runs.
36
+
37
+ - Keep a read-only run in the checkout and never switch its branch, since a switch changes the files under every other session reading it
38
+ - Serve a preview on a free port only when it writes nothing, and run one that builds from a temporary copy, since a build leaves caches and output under the checkout
39
+ - Send a file made only to look at, such as a capture or a dump, to a scratch topic slug under the scratch rule, which owns the path and the reserved slugs
40
+ - Put an own-project fixture or headless session outside the repository, since a tool that walks up to a project root measures the enclosing repository
41
+ - Keep the plan's own text as the record of what a run showed, since scratch is gitignored and no later session reads it
42
+
43
+ ### Name the binary a run measured
44
+
45
+ A run against the toolkit itself can reach the installed binary or the checkout's own build, and the two answer differently once the branch has moved. State which one ran, with its version or path, beside the command in `## Current behavior`.
46
+
47
+ ## Record a run that did not happen
48
+
49
+ Write the surface as not run with the reason, such as a broken build, a preview that will not start, or a verb with no read form that no sandbox scenario or temporary copy can reach. A skipped run with no record reads the same as one nobody considered.
50
+
51
+ Step 3 writes each run, or its not-run reason, into `## Current behavior`.
@@ -72,6 +72,7 @@ The document opens with `# Feature: <short title>` and one paragraph stating wha
72
72
  | Section | Holds | Required |
73
73
  | ---------------------- | --------------------------------------------------------------------------- | ------------------------------------------------- |
74
74
  | `## Summary` | three to five one-line bullets: the goal, the deliverables, the trade-off | Always |
75
+ | `## Current behavior` | each command run against the thing being changed and what it showed | When the change touches something that runs |
75
76
  | `## Constraints` | durable rules the work must respect | When supplied |
76
77
  | `## Files to touch` | each file with a one-line reason | Always |
77
78
  | `## Design references` | the tracked design folders the change builds against, each with its surface | When Files to touch names a rendered-surface file |
@@ -81,6 +82,8 @@ The document opens with `# Feature: <short title>` and one paragraph stating wha
81
82
  | `## Questions` | numbered open decisions, each with a suggestion and an answer slot | Always |
82
83
  | `## Sources` | the outside sources a decision rests on, each with its quoted passage | When a decision rests on an outside source |
83
84
 
85
+ ### Writing a section
86
+
84
87
  Four rules govern how a section is written:
85
88
 
86
89
  - Write each section as the H2 the table gives, so a reader scans the plan by its outline
@@ -126,6 +129,15 @@ State every count and every claim about the tree as measured during the pass tha
126
129
 
127
130
  Prefer a short plan over a padded one. A section filled to look thorough costs the reader the same attention as one that matters.
128
131
 
132
+ ### Current behavior
133
+
134
+ A plan changing a verb, a route, or a rendered surface starts from what that thing does today, so the section records the run the planner made and not a reading of the source.
135
+
136
+ - Write one bullet per run, leading with the command or request in backticks and ending on what it showed
137
+ - Run only a read form, per the safe forms in `onboarding.md`, since a planner writes its plan and nothing else
138
+ - Record a run that could not happen as not run with the reason. Never leave it out silently.
139
+ - Leave the section out of a plan touching nothing that runs, such as prose or a config seed
140
+
129
141
  ### Constraints
130
142
 
131
143
  Work in flight reaches a plan as a hold or as a shared path. A hold leaves a surface alone and says which of two acts it forbids, rather than leaving the executing session to guess. Neither act is forbidden over a shared path.
@@ -259,6 +271,10 @@ Each H3 below shows where a section groups once it passes 5 bullets. A section o
259
271
  - <one-line bullet covering the main deliverables>
260
272
  - <one-line bullet covering the key decision or trade-off>
261
273
 
274
+ ## Current behavior
275
+
276
+ - `<command or request>`: <what it showed, or not run and why>
277
+
262
278
  ## Constraints
263
279
 
264
280
  Measured against `<commit>` on <YYYY-MM-DD>.
@@ -98,7 +98,7 @@ Read these in parallel from the project root, skipping any that do not exist:
98
98
  - `canon/REQUIREMENTS.md`: scope and non-goals
99
99
  - `.canon/tasks/index.md`: what is already tracked, and what a prior track concluded. Open a task file only when its entry looks related.
100
100
 
101
- Then read only what a live question needs, never whole directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
101
+ Then read only what a live question needs, never whole directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at. For each domain the topic touches, read its context entry ahead of measuring, through the lookup in `${CLAUDE_SKILL_DIR}/../plan-feature/references/onboarding.md`.
102
102
 
103
103
  Do not dispatch subagents. A groundwork track is a conversation, and fanning out loses the reasoning that makes the folder worth keeping. A search too large to run inline is a finding that the question is too broad.
104
104
 
@@ -7,15 +7,15 @@ description: Why a brain dump gets a filed inventory rather than ten plans, and
7
7
 
8
8
  ## Gap
9
9
 
10
- A brain dump reaches a session with nowhere to put it. One plan per concern turns forty findings into ten plans before anything is measured, and what gets filed instead is a list of opinions, because nothing forces a count against the tree.
10
+ A brain dump reaches a session with nowhere to put it. One plan per concern turns forty findings into ten plans before anything is measured, and what gets filed instead is a list of opinions, because nothing forces a count against the tree. A defect confirmed from source alone reads the same as one that was run.
11
11
 
12
12
  An empty operator slot reads as consent under a blank-means-accept contract, which ships changes nobody approved. A report naming only a path hides what moved. A question filed without a pick comes back unresolved, and a pass with no write scope starts fixing what it files.
13
13
 
14
14
  ## Must
15
15
 
16
16
  - Route each item on whether the repository can answer it today, sending experiments to `plan-groundwork` and decided work to `plan-feature`
17
- - Measure every problem line against the tree, close every item with a verdict, and pair every open question with a suggested pick
18
- - Treat an empty operator slot as unread, and keep answers on items with the index pointing at them
17
+ - Measure every problem line against the tree, running a defect's one-command repro read-only or marking it not reproduced here
18
+ - Close every item with a verdict and a suggested pick on each question, and treat an empty operator slot as unread
19
19
  - Reserve the index number, carry the domain in every other filename, and number a new folder past the highest ordinal across intake and groundwork
20
20
  - Confine writes to the intake folder and name the heading and the act beside every path written
21
21
 
@@ -63,10 +63,14 @@ Read these in parallel from the project root, skipping any that do not exist:
63
63
 
64
64
  Then read only what a live item needs, never whole directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
65
65
 
66
+ For each domain an item touches, read its context entry through the lookup in `${CLAUDE_SKILL_DIR}/../plan-feature/references/onboarding.md`, since the entry records the traps a grep will not show.
67
+
66
68
  ## Step 3: measure against the tree
67
69
 
68
70
  Grep for each construct an item names and count the sites. Every problem line carries a number or a file path taken during this pass.
69
71
 
72
+ When an item reports a defect with a one-command repro, run it in a read-only form and say on the `Problem:` line what it showed, or that it was not reproduced here. Read `${CLAUDE_SKILL_DIR}/references/intake.md` for where one command ends and groundwork begins.
73
+
70
74
  - Never carry a figure from an earlier session, a summary, or another document. The tree states the size of a complaint, and that is what separates an inventory from opinions.
71
75
  - Confirm any work an item sequences behind is still open, so no item leads with something already shipped
72
76
  - Name the commit the pass measured against in the overview body, since that is the half a later reader can check
@@ -19,6 +19,7 @@ The folder is gitignored. Where a records remote is configured, `canon records p
19
19
  - [Files](#files)
20
20
  - [Frontmatter and dating](#frontmatter-and-dating)
21
21
  - [00-overview.md](#00-overviewmd) <!-- canon-allow-reference: a heading anchor, not a pull request number -->
22
+ - [Repro versus groundwork](#repro-versus-groundwork)
22
23
  - [Item format](#item-format)
23
24
  - [The answer contract](#the-answer-contract)
24
25
  - [Retrieval](#retrieval)
@@ -99,6 +100,15 @@ The index carries no answer slot. One question in two answerable places has no r
99
100
 
100
101
  Where an item touches a task already on the board, say so in the index rather than only inside the item. A reader deciding what to promote reads the index first.
101
102
 
103
+ ## Repro versus groundwork
104
+
105
+ An item whose claim is a defect may carry one run of its repro, and the run is where intake's measuring stops. A repro that needs more is groundwork's.
106
+
107
+ - Run one command in a form that writes nothing, per `${CLAUDE_SKILL_DIR}/../plan-feature/references/onboarding.md`, and put what it showed on `Problem:`
108
+ - Mark the item `not reproduced here` on the same line when the repro needs a fixture project, repeated runs, a write, or another project's paths
109
+ - Send the marked item to `plan-groundwork` only when its answer decides a plan, since a claim read from code alone is a reported defect and not a confirmed one
110
+ - Never adapt a repro silently, because a changed command measures something the reporter did not
111
+
102
112
  ## Item format
103
113
 
104
114
  ```markdown
@@ -82,7 +82,7 @@ This skill owns the reply, so do not reimplement the followup flow here.
82
82
 
83
83
  - `git-followup` syncs the body and title on every invocation, `reply-owned` included, so the merge record reflects this pass. The reply still maps fixes to findings in the review's own thread.
84
84
  - A rebase in step 5 rewrote the branch, so the push is a force-push `git-followup` resolves from the tracking branch. Worker branches are single-owner, which makes that safe.
85
- - `git-followup` stops on an unchanged tree, as when every finding was a conscious-accept and the rebase left generated files alone. Push that with `git push --force-with-lease`.
85
+ - `git-followup` runs on a clean tree too, since the rebased commits are ahead of the tracking branch. A pass of conscious-accepts after a rebase pushes through it and syncs the body like any other.
86
86
 
87
87
  Once the fixes are commits, re-run step 5's staleness test against the new head. A conflict appearing only now is one the fixes introduced against lines `main` moved, so rebase again under `${CLAUDE_SKILL_DIR}/references/rebase-conflicts.md` and force-push.
88
88
 
@@ -17,6 +17,7 @@ The verbs report every fact, so the gap is the judgment between them. A session
17
17
  - Read every fact from a `canon` verb, so a later release changes what the skill reports with no edit to its body
18
18
  - Stop and ask on a refused migration, a stranded file, an unattributed folder, and a rule the stack newly ships
19
19
  - Keep a customized file and a drifted seed as the project wrote them, naming the toolkit change each one skipped
20
+ - Keep the shared checkout on its branch, writing on a worktree or a declined-worktree branch the preview names
20
21
  - End on a clean `target-check` or on named remainders, with nothing written when nothing is pending
21
22
 
22
23
  ## Must not
@@ -66,6 +66,8 @@ Target update: `<path>`, canon `<installed>` to `<running>`.
66
66
 
67
67
  **Drift:** <each non-empty cause from sync --check, with its count and paths>
68
68
 
69
+ **Where it lands:** <the linked worktree this run is already in, or the worktree `.claude/worktrees/<name>/` it will enter, or the branch it will create in place when the operator declines a worktree>
70
+
69
71
  **Calls for you:** <each judgment call below this run meets, with the default this skill takes>
70
72
 
71
73
  **Commands, in order:** <the exact commands Steps 4 to 6 will run>
@@ -77,6 +79,26 @@ Summarize the changelog for this project, not for the toolkit. Drop an entry tha
77
79
 
78
80
  When the changelog range is empty, `migrate status` lists nothing pending or refused, and the drift lines hold nothing a step here would take, write nothing. Invoke `canon:target-check` and report the target current with its table.
79
81
 
82
+ ### Take a branch after the go-ahead
83
+
84
+ The migrations, the sync, and the commits write the project, so they land on their own branch rather than on the one the shared checkout holds, where every other session in it would follow. Step 1 stays where it ran, since it writes the machine rather than the project.
85
+
86
+ Compare `git rev-parse --git-dir` with `git rev-parse --git-common-dir`, then take the first row that fits:
87
+
88
+ | State | This skill |
89
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
90
+ | Different, so already in a linked worktree | Uses it and enters nothing, since a second worktree would nest inside the first |
91
+ | Equal, and the operator took the worktree | Invokes `canon:session-worktree` with `chore/update-canon`, then runs Steps 4 to 6 from inside it |
92
+ | Equal, declined, on `main` or `master` | Invokes `canon:git-branch` for `chore/update-canon`, then runs Steps 4 to 6 in place |
93
+ | Equal, declined, on any other branch | Stops and says so, since `canon:git-branch` would rename that branch under every session on it |
94
+
95
+ Four points govern the choice:
96
+
97
+ - Offer the worktree as the default in the preview's `Where it lands` line, and take a decline from the operator's reply to it
98
+ - Report it rather than proceeding silently when `canon:session-worktree` does not resolve, as in a project that installed governance without the plugin, then fall back to `canon:git-branch`
99
+ - Stop when neither skill resolves, saying the run would otherwise write onto the shared checkout's branch
100
+ - Run the Guards' dirty-tree test again from the new tree. A fresh worktree starts clean, so a refusal there is a real one.
101
+
80
102
  ## Step 4: migrate
81
103
 
82
104
  Run each pending migration in the order `migrate status` gave, one at a time, as the entry's `verb` with `--write --json` appended. The verb carries the flags its dry run used, so a bare `canon migrate <name>` would apply a plan the preview never showed.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.56.0",
4
+ "version": "5.57.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -668,12 +668,15 @@ export function register(program: Command): void {
668
668
  ' no-board the repository holds no `.canon/tasks/` board',
669
669
  ' no-match no task matches the selector',
670
670
  ' ambiguous more than one task matches the selector',
671
- ' bad-input a task and a plan both named, or neither',
671
+ ' bad-input a task and a plan both named',
672
672
  '',
673
673
  'Exit codes:',
674
674
  ' 0 the task is read, whether or not it closes',
675
675
  ' 1 refused, with the reason on stderr or in the JSON record',
676
676
  '',
677
+ 'With no task or plan named, the task whose plan derives the branch is read,',
678
+ 'and a branch nothing owns is no-match.',
679
+ '',
677
680
  'Reads the `Issue:` lines, each `#NNN` or a comma list, and reports',
678
681
  '`issues` and `closing`. The pull request closes them only when it is',
679
682
  "the task's last slice, the condition the merge-time archive applies:",
@@ -1513,15 +1516,18 @@ async function runCloses(
1513
1516
  opts: ClosesCommandOptions,
1514
1517
  ): Promise<number> {
1515
1518
  const emitJson = opts.json ?? false
1516
- const selector = selectorFor(task, opts.plan)
1519
+ const branch = opts.branch ?? (await workingBranch())
1520
+ const branches = branch ? [branch, ...(await formerNames(branch))] : []
1521
+ const selector =
1522
+ !task && !opts.plan
1523
+ ? ({ kind: 'branch', branches } as const)
1524
+ : selectorFor(task, opts.plan)
1517
1525
 
1518
1526
  if ('ok' in selector) {
1519
1527
  return reportRecord('canon tasks closes', selector, emitJson, process.cwd())
1520
1528
  }
1521
1529
 
1522
1530
  const root = opts.root ?? (await mainWorktreeRoot())
1523
- const branch = opts.branch ?? (await workingBranch())
1524
- const branches = branch ? [branch, ...(await formerNames(branch))] : []
1525
1531
  const outcome = await readClosure(
1526
1532
  root,
1527
1533
  selector,
@@ -150,6 +150,7 @@ function coversIgnoreAddition(
150
150
  * path written short from a path written wrong.
151
151
  */
152
152
  function covers(claim: PathClaim, path: string): boolean {
153
+ if (claim.bare) return path === claim.path
153
154
  if (claim.directory) {
154
155
  return claim.anchored
155
156
  ? path.startsWith(claim.path)
@@ -212,6 +213,8 @@ export function compareKeyChanges(input: BijectionInput): Bijection {
212
213
  continue
213
214
  }
214
215
 
216
+ if (claim.bare) continue
217
+
215
218
  if (claim.anchored && claim.leading && !evidenceUnread) unmet.push(claim)
216
219
  else unresolved.push(claim)
217
220
  }
@@ -240,9 +243,8 @@ export function compareKeyChanges(input: BijectionInput): Bijection {
240
243
  * and an unanchored claim never accuses, so the first branch to open one would
241
244
  * silently lose the graded direction.
242
245
  *
243
- * A path with no folder above it contributes nothing, since a claim carrying no
244
- * slash never reaches the extractor's output and no root would ever be read
245
- * against it.
246
+ * A path with no folder above it contributes nothing, since a bare claim is
247
+ * never anchored and no root would ever be read against it.
246
248
  */
247
249
  export function treeRoots(
248
250
  tracked: readonly string[],
package/src/pr/paths.ts CHANGED
@@ -36,6 +36,16 @@ export interface PathClaim {
36
36
  * accuse with.
37
37
  */
38
38
  readonly leading: boolean
39
+ /**
40
+ * True when the span carried no slash, such as `README.md` or `bun`.
41
+ *
42
+ * A bare claim credits a changed file whose whole path is exactly that name
43
+ * and nothing else, so `package.json` never reaches `web/package.json`. It
44
+ * never accuses: nothing separates a bare file name from a word of prose, so
45
+ * one matching no changed file is reported nowhere. Same asymmetry as
46
+ * `anchored` and `leading`.
47
+ */
48
+ readonly bare: boolean
39
49
  /** The span exactly as the body wrote it, before the line suffix came off. */
40
50
  readonly span: string
41
51
  /** One-based index of the bullet inside the section. */
@@ -282,17 +292,22 @@ function splitBullets(section: string): string[] {
282
292
  interface ResolvedSpan {
283
293
  readonly path: string
284
294
  readonly directory: boolean
295
+ readonly bare: boolean
285
296
  }
286
297
 
287
298
  function resolveSpan(span: string): ResolvedSpan | undefined {
288
299
  if (span === '' || isNotRepositoryPath(span)) return undefined
289
300
 
290
- // A bare name is the fourth observed false-positive class and it drops
291
- // outright. Resolved as a sibling of a path earlier in the bullet it produced
292
- // seven wrong paths across this corpus against two right ones, because a
293
- // compound bullet names a sibling folder as often as a sibling file. Dropping
294
- // it under-reports in the unnamed direction and never fires in the other.
295
- if (!span.includes('/')) return undefined
301
+ // A bare name cannot be resolved as a sibling of a path earlier in the
302
+ // bullet: that produced seven wrong paths across this corpus against two right
303
+ // ones, because a compound bullet names a sibling folder as often as a sibling
304
+ // file. It resolves to itself as a root-level path and is marked bare, which
305
+ // keeps it from ever accusing.
306
+ if (!span.includes('/')) {
307
+ return isDottedNumber(span)
308
+ ? undefined
309
+ : { path: span, directory: false, bare: true }
310
+ }
296
311
 
297
312
  if (span.endsWith('/')) {
298
313
  // A single top-level folder is never a claim. Nobody reports having changed
@@ -300,10 +315,12 @@ function resolveSpan(span: string): ResolvedSpan | undefined {
300
315
  // something lives.
301
316
  return span.indexOf('/') === span.length - 1
302
317
  ? undefined
303
- : { path: span, directory: true }
318
+ : { path: span, directory: true, bare: false }
304
319
  }
305
320
 
306
- return isDottedNumber(span) ? undefined : { path: span, directory: false }
321
+ return isDottedNumber(span)
322
+ ? undefined
323
+ : { path: span, directory: false, bare: false }
307
324
  }
308
325
 
309
326
  /**
@@ -371,7 +388,9 @@ export function extractKeyChangePaths(
371
388
 
372
389
  const resolved = resolveSpan(bare)
373
390
  if (resolved === undefined) continue
374
- claimed = true
391
+ // A bare span is no evidence a path claim came first, so it leaves the
392
+ // citation guard above reading only the claims it was written for.
393
+ if (!resolved.bare) claimed = true
375
394
  // First occurrence wins, across bullets as well as inside one. A path
376
395
  // named trailing here and leading three bullets later keeps the weaker
377
396
  // reading, which loses an accusation and can never add one.
@@ -381,8 +400,11 @@ export function extractKeyChangePaths(
381
400
  claims.push({
382
401
  path: resolved.path,
383
402
  directory: resolved.directory,
384
- anchored: roots.has(resolved.path.slice(0, resolved.path.indexOf('/'))),
403
+ anchored:
404
+ !resolved.bare &&
405
+ roots.has(resolved.path.slice(0, resolved.path.indexOf('/'))),
385
406
  leading: match.start < accusesBefore,
407
+ bare: resolved.bare,
386
408
  span,
387
409
  bullet: index + 1,
388
410
  preview,
@@ -219,6 +219,7 @@ const QUESTION_ITEM = /^\d+[a-z]?\.[ \t]+\S/
219
219
 
220
220
  const PLAN_SECTIONS = [
221
221
  'Summary',
222
+ 'Current behavior',
222
223
  'Constraints',
223
224
  'Files to touch',
224
225
  'Design references',
@@ -43,6 +43,11 @@ function slugOf(path: string): string {
43
43
  return stem.startsWith(PLAN_PREFIX) ? stem.slice(PLAN_PREFIX.length) : stem
44
44
  }
45
45
 
46
+ /** The branch a plan file derives, from its filename alone. */
47
+ export function branchOfPlan(path: string): string {
48
+ return `${PLAN_BRANCH_TYPE}/${slugOf(path)}`
49
+ }
50
+
46
51
  /**
47
52
  * Derives the branch a dispatch checks and a worker takes, from the plan both
48
53
  * of them name. It is the one derivation, so the collision check and the
@@ -60,7 +65,7 @@ export function planBranch(root: string, reference: string): BranchOutcome {
60
65
  if (!resolved.ok) return resolved
61
66
 
62
67
  const slug = slugOf(resolved.path)
63
- const branch = `${PLAN_BRANCH_TYPE}/${slug}`
68
+ const branch = branchOfPlan(resolved.path)
64
69
  const words = slug.split('-').filter((word) => word.length > 0).length
65
70
 
66
71
  return {
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile, writeFile } from 'node:fs/promises'
3
3
  import { basename, join, relative } from 'node:path'
4
4
  import { planCandidates } from '@/tasks/answers'
5
+ import { branchOfPlan } from '@/tasks/branch'
5
6
  import {
6
7
  appendPlanLink,
7
8
  describeUnmatchedStem,
@@ -57,6 +58,7 @@ export interface RecordRefused {
57
58
  export type RecordSelector =
58
59
  | { readonly kind: 'stem'; readonly stem: string }
59
60
  | { readonly kind: 'plan'; readonly plan: string }
61
+ | { readonly kind: 'branch'; readonly branches: readonly string[] }
60
62
 
61
63
  export type LineAction = 'added' | 'appended' | 'corrected' | 'unchanged'
62
64
 
@@ -383,6 +385,25 @@ async function resolveStem(
383
385
  return selector.stem
384
386
  }
385
387
 
388
+ if (selector.kind === 'branch') {
389
+ const matched = await matchByBranch(dir, stems, selector.branches)
390
+ const label = selector.branches[0] ?? 'no branch'
391
+
392
+ if (matched.length === 0) {
393
+ return refuse('no-match', `No task's plan derives branch ${label}.`)
394
+ }
395
+
396
+ if (matched.length > 1) {
397
+ return refuse(
398
+ 'ambiguous',
399
+ `${matched.length} tasks derive branch ${label}. One task, one plan.`,
400
+ matched,
401
+ )
402
+ }
403
+
404
+ return matched[0]
405
+ }
406
+
386
407
  const matched = await matchByPlan(dir, stems, selector.plan)
387
408
 
388
409
  if (matched.length === 0) {
@@ -400,6 +421,29 @@ async function resolveStem(
400
421
  return matched[0]
401
422
  }
402
423
 
424
+ /**
425
+ * Matches a branch to the tasks whose plan derives it. A task with no `Plan:`
426
+ * line carries no target and so never matches.
427
+ */
428
+ async function matchByBranch(
429
+ dir: string,
430
+ stems: readonly string[],
431
+ branches: readonly string[],
432
+ ): Promise<string[]> {
433
+ const read = await Promise.all(
434
+ stems.map(async (stem) => ({
435
+ stem,
436
+ targets: readPlanTargets(await readFile(join(dir, `${stem}.md`), 'utf8')),
437
+ })),
438
+ )
439
+
440
+ return read
441
+ .filter((entry) =>
442
+ entry.targets.some((target) => branches.includes(branchOfPlan(target))),
443
+ )
444
+ .map(({ stem }) => stem)
445
+ }
446
+
403
447
  export async function openTask(
404
448
  root: string,
405
449
  selector: RecordSelector,