@erclx/canon 5.56.0 → 5.58.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 +28 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-feedback-triage/SKILL.md +7 -5
- package/claude/skills/context-fold/references/context.md +1 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -2
- package/claude/skills/document-health/SKILL.md +14 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/document-repair/REQUIREMENT.md +6 -1
- package/claude/skills/document-repair/SKILL.md +21 -0
- 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 +18 -2
- 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/docs/agents/context-audit.md +1 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/rules/ui/design-taste.md +4 -0
- package/package.json +1 -1
- package/src/commands/tasks.ts +10 -4
- package/src/context/audit.ts +26 -1
- 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,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.58.0](https://github.com/erclx/canon/compare/v5.57.0...v5.58.0) (2026-10-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **governance:** widen design-taste paths to component files ([#2645](https://github.com/erclx/canon/issues/2645)) ([01b1445](https://github.com/erclx/canon/commit/01b144514eced1150c90b18acc78f0be33990a23))
|
|
9
|
+
* **rules:** route interface changes to api-design ([#2647](https://github.com/erclx/canon/issues/2647)) ([09a073c](https://github.com/erclx/canon/commit/09a073ca38324a399de5c2c2e702fbcede6bb38c))
|
|
10
|
+
* **skills:** offer a restructure before document-repair rewrites ([#2648](https://github.com/erclx/canon/issues/2648)) ([7c2776a](https://github.com/erclx/canon/commit/7c2776a95b86a6626c44e399056b9eaa22418d65))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **context:** count table rows toward no folder paragraph ([#2646](https://github.com/erclx/canon/issues/2646)) ([a99b020](https://github.com/erclx/canon/commit/a99b02064105b81bc3a4f6e4820c73198942785e))
|
|
16
|
+
|
|
17
|
+
## [5.57.0](https://github.com/erclx/canon/compare/v5.56.0...v5.57.0) (2026-10-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **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))
|
|
23
|
+
* **plan-feature:** run the existing surface before planning ([#2634](https://github.com/erclx/canon/issues/2634)) ([53c4b93](https://github.com/erclx/canon/commit/53c4b936ed89d42122e251f638e87791278ef883))
|
|
24
|
+
* **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))
|
|
25
|
+
* **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))
|
|
26
|
+
* **skills:** run a repro during intake and triage ([#2638](https://github.com/erclx/canon/issues/2638)) ([82e1fb4](https://github.com/erclx/canon/commit/82e1fb4bdb54eefcea910cf745c80623a8f8368c))
|
|
27
|
+
* **skills:** triage asks plan, ask, or decline ([#2641](https://github.com/erclx/canon/issues/2641)) ([2502248](https://github.com/erclx/canon/commit/2502248fe5ce4ad7dd11a4b27c8f4297ba654fa1))
|
|
28
|
+
* **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))
|
|
29
|
+
* **tasks:** resolve closes from the checked-out branch ([#2637](https://github.com/erclx/canon/issues/2637)) ([873834c](https://github.com/erclx/canon/commit/873834c69b8de11efe16bbdaa06187b9328e9738))
|
|
30
|
+
|
|
3
31
|
## [5.56.0](https://github.com/erclx/canon/compare/v5.55.0...v5.56.0) (2026-10-23)
|
|
4
32
|
|
|
5
33
|
|
|
@@ -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.
|
|
@@ -71,7 +71,7 @@ An entry carries three H2 sections and no other, being `Overview`, `Layout`, and
|
|
|
71
71
|
|
|
72
72
|
Layout gives each folder doing real work its own H3, titled with the folder path in a code span, such as ``### `src/design/` ``.
|
|
73
73
|
|
|
74
|
-
- Open the H3 with a one-paragraph lead stating what the folder does, then at most five bullets on what it does now
|
|
74
|
+
- Open the H3 with a one-paragraph lead stating what the folder does, then at most five bullets on what it does now. A table after the lead counts toward neither, with one sentence to lead the bullets
|
|
75
75
|
- Put every trivial folder under `### Other folders` as a one-line bullet in owns-X form
|
|
76
76
|
- Name folders only, never files, and write no decisions and no history under any H3
|
|
77
77
|
- Give each folder under the code roots one H3 or `Other folders` line, since a second entry may cite it but never claims it
|
|
@@ -13,8 +13,8 @@ Length, placement, and staleness answer to different evidence, and one list hand
|
|
|
13
13
|
|
|
14
14
|
## Must
|
|
15
15
|
|
|
16
|
-
- Report per document across the length, placement, and staleness axes, marking each finding with its evidence class
|
|
17
|
-
-
|
|
16
|
+
- Report per document across the length, placement, and staleness axes, marking each finding with its evidence class, read through the verb owning each measurement and its record's checkpoints
|
|
17
|
+
- Propose a split or an owner as a structure line for each overgrown entry and doubled or unnamed coverage finding, with the headings and the reason
|
|
18
18
|
- Ship the staleness axis as a reading rather than cutting it for lacking a verb
|
|
19
19
|
- Report a refusal, an absent key, or a missing subcommand as an unread axis naming the cause, and say which layer answered the classifier sweep
|
|
20
20
|
- Run standalone, read the requirements record for identity drift when the scope reaches it, and print the `canon records stale canonical` reminder
|
|
@@ -47,6 +47,12 @@ The first names the backend, the model, and the source that decided both. Report
|
|
|
47
47
|
|
|
48
48
|
Take the checkpoints from the record each verb returns rather than from a number written here. A checkpoint the CLI moves reaches this report with no edit, and a number held in this body goes stale on its own cadence.
|
|
49
49
|
|
|
50
|
+
### The structure line
|
|
51
|
+
|
|
52
|
+
The context audit record already carries what a restructure needs, so the placement axis gains a structure line per area folder and runs no further verb. `${CLAUDE_SKILL_DIR}/references/axes.md` holds how to read `length` and `coverage` into a proposed split or owner.
|
|
53
|
+
|
|
54
|
+
Emit the line only for a folder with an entry past the line checkpoint whose Layout headings group into two or more domains, or with a doubled or unnamed coverage finding. A folder with neither gets no line, and `document-repair` goes straight to its rewrite.
|
|
55
|
+
|
|
50
56
|
### An unread axis is not a healthy one
|
|
51
57
|
|
|
52
58
|
A refusal, an absent key, and a subcommand the project's binary does not carry each leave an axis unread. Report it as unread against the documents it would have covered, name the read that could not be taken, and name the cause.
|
|
@@ -106,4 +112,12 @@ When Step 3 read the canonical records, add one reminder line per doc the verb r
|
|
|
106
112
|
|
|
107
113
|
Write `reviewed value is not a date: <value>` for a doc carrying `invalidReviewed`, and `no release tags to count` in place of the count when `tagged` is false. Name the unread cause instead when the verb refused or the binary carries no `canonical` kind.
|
|
108
114
|
|
|
115
|
+
Under the table, add one structure line per area folder that earned one, naming the entry and the split, or the finding and its owner:
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
Structure `canon/context/surfaces/`: `cli.md` (300 lines) splits into `cli.md` taking <headings> and `cli-<domain>.md` taking <headings>, because <reason>.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The line is a proposal for the operator, with the headings each entry takes and why. Health applies nothing, and `document-repair` offers the proposal before it rewrites.
|
|
122
|
+
|
|
109
123
|
Keep the evidence line under the table on every run. A table read without it flattens a judgment into a measurement, which is the one shape the three classes exist to prevent.
|
|
@@ -54,6 +54,18 @@ The sweep classifies each section as keep, replace or rewrite, history, or move.
|
|
|
54
54
|
- A low-confidence `move` is a prompt to look rather than an instruction to cut
|
|
55
55
|
- **Routes to:** the document the verdict names as the owner. Where the move crosses a tier rather than a file, the project's own content-ownership rules decide the destination.
|
|
56
56
|
|
|
57
|
+
### The structure line
|
|
58
|
+
|
|
59
|
+
The audit record's `length` array lists each entry past the line checkpoint, and `coverage.findings` lists the `doubled` and `unnamed` ones. The structure line reads both, so it carries the `measured` class for the finding and `read` for the proposal built on it.
|
|
60
|
+
|
|
61
|
+
- **Overgrown entry:** read its `causes` in `length` first, since `history: yes` points at cutting provenance and one domain at a trim, so neither earns a split
|
|
62
|
+
- **Split:** group the Layout headings by domain, one new entry per group beyond the first, naming each entry's headings and one reason
|
|
63
|
+
- **Domain boundaries:** no verb measures one, so state the grouping as a judgment.
|
|
64
|
+
- **Doubled coverage:** two entries describe one source area. Propose the one that owns it, from which entry already holds the area's traps, and the other points at it.
|
|
65
|
+
- **Unnamed coverage:** no entry names the area. Propose an owner among the existing entries before a new one, since a new entry is the larger change.
|
|
66
|
+
|
|
67
|
+
A proposal names headings and a reason, so an operator judges a concrete split. A split nobody can act on names none of those.
|
|
68
|
+
|
|
57
69
|
### When a verb resolves nothing
|
|
58
70
|
|
|
59
71
|
Both verbs resolve their folders by surface root rather than by a path written into them, so a project laid out differently resolves nothing. The two degrade differently.
|
|
@@ -15,14 +15,19 @@ A folder repaired by hand also drifts back, since a target gates nothing until a
|
|
|
15
15
|
|
|
16
16
|
- Each batch takes one folder and leaves it at zero findings or reports it ungated
|
|
17
17
|
- A folder gates only through `canon markdown gated add`, which refuses one with a finding left
|
|
18
|
-
- A rewrite keeps every stated reason, command, and path the original carried
|
|
18
|
+
- A rewrite keeps every stated reason, command, and path the original carried, and leaves a context entry no more shape findings than it started with
|
|
19
19
|
- The body names the verbs and states no limit number
|
|
20
20
|
- A requirement gates by its own path, never through a folder prefix
|
|
21
21
|
|
|
22
|
+
## Restructure first
|
|
23
|
+
|
|
24
|
+
An overgrown context folder is offered the restructure `document-health` proposes before any rewrite, and the restructure applies only on the operator's word.
|
|
25
|
+
|
|
22
26
|
## Must not
|
|
23
27
|
|
|
24
28
|
- Edit `canon/config/markdown.toml` by hand
|
|
25
29
|
- Report a folder gated that the verb refused
|
|
30
|
+
- Reach zero density findings by growing the context shape findings
|
|
26
31
|
- Change what a document claims to pass the audit
|
|
27
32
|
- Be reachable only by its name, since `document-health` names it for every length finding
|
|
28
33
|
|
|
@@ -18,6 +18,15 @@ The CLI holds every number. `canon markdown audit` measures, and `canon markdown
|
|
|
18
18
|
- `gated add` refusing with `canon-checkout`: stop. Canon's own checkout gates through a constant in its source, which a code change edits.
|
|
19
19
|
- Branch on each record's own fields rather than on an exit status a shell wrapper can flatten. A call with no terminal on stdin already runs headless, so no command takes a prefix.
|
|
20
20
|
|
|
21
|
+
## Step 0: offer the restructure
|
|
22
|
+
|
|
23
|
+
Restructure an overgrown context folder before rewriting it, so the effort is spent once. Run this step only when `canon/context/` is a candidate, meaning it is not yet listed in `canon/config/markdown.toml`, and skip it silently otherwise.
|
|
24
|
+
|
|
25
|
+
- Take the structure line `document-health` reports for each area folder, or read it from `canon context audit --json` the way that skill does
|
|
26
|
+
- A folder with no overgrown entry and no doubled or unnamed coverage finding has no line, so go straight to Step 1 with no prompt
|
|
27
|
+
- Show the operator each proposal and ask before applying it, since a split moves files other entries cite. Re-run the audit afterward to confirm the citations still resolve.
|
|
28
|
+
- Take a declined proposal as a decision and rewrite the entry as it stands
|
|
29
|
+
|
|
21
30
|
## Step 1: read the audit
|
|
22
31
|
|
|
23
32
|
Run the audit over the whole tree and take the density and reference shape findings from its record:
|
|
@@ -26,6 +35,12 @@ Run the audit over the whole tree and take the density and reference shape findi
|
|
|
26
35
|
canon markdown audit --json
|
|
27
36
|
```
|
|
28
37
|
|
|
38
|
+
When `canon/context/` is a candidate, read the context shape beside it and keep each entry's `shapeFindings` issues as the baseline Step 3 holds the rewrite to:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
canon context audit --json
|
|
42
|
+
```
|
|
43
|
+
|
|
29
44
|
Take the candidate folders from the order table in `${CLAUDE_SKILL_DIR}/references/batches.md` rather than from the findings, since a clean folder carries no finding and would never be gated. Drop every folder `canon/config/markdown.toml` already lists, since a listed folder already gates.
|
|
30
45
|
|
|
31
46
|
Sort what is left in two. A candidate the findings never name needs no rewrite, so gate it through Step 4 before the first batch. Group the findings under the rest, which Steps 2 and 3 rewrite.
|
|
@@ -46,6 +61,12 @@ canon markdown audit <prefix> --json
|
|
|
46
61
|
|
|
47
62
|
Read `${CLAUDE_SKILL_DIR}/references/batches.md` for what a rewrite may cut. A stated reason survives every rewrite, and the audit reaching zero cannot see one that went missing.
|
|
48
63
|
|
|
64
|
+
### Hold the context shape
|
|
65
|
+
|
|
66
|
+
For a file under a folder the context audit resolves, also re-run `canon context audit --json` and compare the entry's `shapeFindings` issues with the baseline. A density fix that splits a Layout paragraph into several can grow `layout-paragraphs`.
|
|
67
|
+
|
|
68
|
+
A rewrite leaving the entry with more shape issues than it started with is not done. Undo it, or fold the split into a table or a linked page, and report the entry as stopped when neither reaches the baseline. Zero density findings reached by growing shape findings only moves the failure.
|
|
69
|
+
|
|
49
70
|
## Step 4: gate the folder
|
|
50
71
|
|
|
51
72
|
```bash
|
|
@@ -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.
|
|
@@ -68,7 +84,7 @@ What this skill adds on top of the standard:
|
|
|
68
84
|
Every `- Suggested:` line takes senior judgment, picking the best option in one line with its reason or main tradeoff, and naming alternatives only when they change the pick.
|
|
69
85
|
|
|
70
86
|
- Suggest a real default when best practice, the codebase, or prior context points to one
|
|
71
|
-
-
|
|
87
|
+
- Before `## Files to touch`, load `canon:codebase-layout` for a new path (its reason) and `canon:api-design` for a changed verb, flag, output, record shape, or endpoint. Report either if missing.
|
|
72
88
|
- Fill `## Design references` with the `canon/design/<surface>/` folder for each surface a UI file in `## Files to touch` renders, and write `None identified.` in a project with no folder
|
|
73
89
|
- Write `## Verification` as one bullet per outcome naming the test or command that proves it
|
|
74
90
|
- Write `## Review focus` as the inputs or states that would break the change, carrying the risk you understood to the executor and the reviewer
|
|
@@ -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.
|
|
@@ -46,7 +46,7 @@ The shape kinds are `retired-section`, `gotcha-count`, `gotcha-heading`, `gotcha
|
|
|
46
46
|
|
|
47
47
|
- A `## Decisions` H2, or a `Gotchas` section past five bullets or holding a heading or paragraph
|
|
48
48
|
- A `Layout` H3 titled neither a code-spanned path nor `Other folders`, or a heading below H3 under `Layout`
|
|
49
|
-
- A folder H3 past five bullets or its one-paragraph lead
|
|
49
|
+
- A folder H3 past five bullets or its one-paragraph lead, where table rows and the sentence leading the bullets after a table count toward neither
|
|
50
50
|
|
|
51
51
|
The development entry may carry a `Scripts` H3 exempt from both folder caps. `SHAPE_GATED_FOLDERS` lives in `src/context/shape-gated.ts`.
|
|
52
52
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Routes code placement and
|
|
2
|
+
description: Routes code placement, shape, and interface decisions to the skills that carry them, and library calls to the official docs
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Code standards
|
|
@@ -12,6 +12,10 @@ description: Routes code placement and shape decisions to the skills that carry
|
|
|
12
12
|
|
|
13
13
|
- Load the `canon:code-craft` skill before deciding the shape of a new function, class, or module, and report it rather than proceeding silently when it does not resolve.
|
|
14
14
|
|
|
15
|
+
## Interfaces
|
|
16
|
+
|
|
17
|
+
- Load `canon:api-design` before shaping a public function, flag or output, record format, endpoint, or caller-handled error, and report it rather than proceeding silently when it does not resolve.
|
|
18
|
+
|
|
15
19
|
## Libraries
|
|
16
20
|
|
|
17
21
|
- Before calling a library API the codebase does not already use, fetch the official docs for the installed version and match the call to them, since recall of an API goes stale.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Route edits that decide how a surface looks to the design-taste skill for the layer ordering, the coherence locks, and the defaults a model reaches for
|
|
3
3
|
paths:
|
|
4
|
+
- '**/*.tsx'
|
|
5
|
+
- '**/*.jsx'
|
|
6
|
+
- '**/*.astro'
|
|
7
|
+
- '**/*.html'
|
|
4
8
|
- '**/*.css'
|
|
5
9
|
- '**/*.scss'
|
|
6
10
|
- 'canon/DESIGN.md'
|
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/context/audit.ts
CHANGED
|
@@ -616,8 +616,16 @@ function shapeIssues(
|
|
|
616
616
|
let inItem = false
|
|
617
617
|
let inParagraph = false
|
|
618
618
|
let afterBlank = false
|
|
619
|
+
let afterTable = false
|
|
620
|
+
let deferred = false
|
|
621
|
+
|
|
622
|
+
const settleDeferred = (): void => {
|
|
623
|
+
if (deferred && folder) folder.paragraphs++
|
|
624
|
+
deferred = false
|
|
625
|
+
}
|
|
619
626
|
|
|
620
627
|
const closeFolder = (): void => {
|
|
628
|
+
settleDeferred()
|
|
621
629
|
if (folder && !exempt.has(folder.text)) {
|
|
622
630
|
if (folder.paragraphs > LAYOUT_PARAGRAPH_CAP) {
|
|
623
631
|
issues.push({
|
|
@@ -661,6 +669,7 @@ function shapeIssues(
|
|
|
661
669
|
inItem = false
|
|
662
670
|
inParagraph = false
|
|
663
671
|
afterBlank = false
|
|
672
|
+
afterTable = false
|
|
664
673
|
|
|
665
674
|
if (level <= 2) {
|
|
666
675
|
closeFolder()
|
|
@@ -697,11 +706,22 @@ function shapeIssues(
|
|
|
697
706
|
inItem = true
|
|
698
707
|
inParagraph = false
|
|
699
708
|
afterBlank = false
|
|
709
|
+
afterTable = false
|
|
710
|
+
deferred = false
|
|
700
711
|
if (gotchas) gotchas.bullets++
|
|
701
712
|
if (folder) folder.bullets++
|
|
702
713
|
continue
|
|
703
714
|
}
|
|
704
715
|
|
|
716
|
+
if (folder && TABLE_ROW.test(line.text)) {
|
|
717
|
+
settleDeferred()
|
|
718
|
+
inItem = false
|
|
719
|
+
inParagraph = false
|
|
720
|
+
afterBlank = false
|
|
721
|
+
afterTable = true
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
|
|
705
725
|
const continues = inItem && (!afterBlank || INDENTED.test(line.text))
|
|
706
726
|
afterBlank = false
|
|
707
727
|
if (continues) continue
|
|
@@ -717,7 +737,12 @@ function shapeIssues(
|
|
|
717
737
|
text: line.text.trim(),
|
|
718
738
|
})
|
|
719
739
|
}
|
|
720
|
-
if (folder)
|
|
740
|
+
if (folder) {
|
|
741
|
+
settleDeferred()
|
|
742
|
+
if (afterTable) deferred = true
|
|
743
|
+
else folder.paragraphs++
|
|
744
|
+
}
|
|
745
|
+
afterTable = false
|
|
721
746
|
}
|
|
722
747
|
|
|
723
748
|
closeFolder()
|
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,
|