@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 +14 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-feedback-triage/SKILL.md +7 -5
- package/claude/skills/git-followup/REQUIREMENT.md +4 -4
- package/claude/skills/git-followup/SKILL.md +8 -2
- package/claude/skills/git-pr/references/closing-issues.md +8 -0
- package/claude/skills/plan-feature/REQUIREMENT.md +2 -1
- package/claude/skills/plan-feature/SKILL.md +17 -1
- package/claude/skills/plan-feature/references/onboarding.md +51 -0
- package/claude/skills/plan-feature/references/plan.md +16 -0
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +3 -3
- package/claude/skills/plan-intake/SKILL.md +4 -0
- package/claude/skills/plan-intake/references/intake.md +10 -0
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/target-update/REQUIREMENT.md +1 -0
- package/claude/skills/target-update/SKILL.md +22 -0
- package/package.json +1 -1
- package/src/commands/tasks.ts +10 -4
- package/src/pr/bijection.ts +5 -3
- package/src/pr/paths.ts +32 -10
- package/src/records/validate.ts +1 -0
- package/src/tasks/branch.ts +6 -1
- package/src/tasks/record.ts +44 -0
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
|
|
|
@@ -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 `
|
|
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>
|
|
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
|
|
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
|
|
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
|
-
-
|
|
17
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
18
|
-
-
|
|
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`
|
|
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
package/src/commands/tasks.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
package/src/pr/bijection.ts
CHANGED
|
@@ -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
|
|
244
|
-
*
|
|
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
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// it
|
|
295
|
-
if (!span.includes('/'))
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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,
|
package/src/records/validate.ts
CHANGED
package/src/tasks/branch.ts
CHANGED
|
@@ -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 =
|
|
68
|
+
const branch = branchOfPlan(resolved.path)
|
|
64
69
|
const words = slug.split('-').filter((word) => word.length > 0).length
|
|
65
70
|
|
|
66
71
|
return {
|
package/src/tasks/record.ts
CHANGED
|
@@ -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,
|