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