@erclx/canon 5.27.0 → 5.28.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 (139) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/draft-ready/SKILL.md +3 -1
  3. package/claude/skills/git-pr/references/evidence.md +1 -1
  4. package/claude/skills/review-address/REQUIREMENT.md +4 -2
  5. package/claude/skills/review-address/SKILL.md +16 -7
  6. package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
  7. package/claude/skills/review-craft/references/ui.md +4 -4
  8. package/claude/skills/review-ui/REQUIREMENT.md +2 -2
  9. package/claude/skills/review-ui/SKILL.md +13 -11
  10. package/claude/skills/review-ui/references/driving.md +8 -8
  11. package/claude/skills/review-ui/references/post.md +13 -3
  12. package/claude/skills/review-ui/references/tick.md +22 -0
  13. package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
  14. package/claude/skills/role-orchestrator/SKILL.md +24 -26
  15. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
  16. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +12 -2
  17. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +1 -1
  19. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
  20. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
  21. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -2
  22. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
  23. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
  24. package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
  25. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  26. package/claude/skills/role-planner/SKILL.md +2 -2
  27. package/claude/skills/role-reviewer/SKILL.md +1 -1
  28. package/claude/skills/role-worker/SKILL.md +3 -2
  29. package/claude/skills/ui-checklist/SKILL.md +1 -1
  30. package/docs/agents/canvas.md +4 -4
  31. package/docs/agents/gate.md +3 -1
  32. package/docs/agents/index.md +2 -1
  33. package/docs/agents/pr-evidence.md +9 -8
  34. package/docs/agents/pr-tick.md +72 -0
  35. package/docs/agents/sandbox.md +2 -2
  36. package/docs/agents/tasks-plans.md +1 -1
  37. package/docs/agents/tasks-validate.md +18 -18
  38. package/docs/agents/tasks.md +1 -1
  39. package/docs/workflow/operating-model.md +10 -15
  40. package/docs/workflow/parallel-features.md +7 -7
  41. package/package.json +1 -1
  42. package/src/audits/baseline.ts +1 -1
  43. package/src/audits/run.ts +1 -1
  44. package/src/canvas/capture.ts +1 -1
  45. package/src/canvas/client/inspector/field.tsx +61 -2
  46. package/src/canvas/client/inspector/fields.css +78 -0
  47. package/src/canvas/client/inspector/token-select.tsx +109 -0
  48. package/src/canvas/client/inspector/values.ts +74 -2
  49. package/src/canvas/client/inspector.tsx +121 -11
  50. package/src/canvas/client/keys.ts +54 -0
  51. package/src/canvas/client/selection.tsx +3 -1
  52. package/src/canvas/client/shell.css +108 -2
  53. package/src/canvas/client/state.ts +96 -5
  54. package/src/canvas/client/surface.tsx +194 -17
  55. package/src/canvas/client/tools.tsx +64 -0
  56. package/src/canvas/content.ts +1 -1
  57. package/src/canvas/edit.ts +12 -2
  58. package/src/canvas/tokens.ts +1 -1
  59. package/src/claude/seeds.ts +2 -2
  60. package/src/claude/skills-reach.ts +1 -1
  61. package/src/commands/canvas.ts +1 -1
  62. package/src/commands/claude.ts +3 -3
  63. package/src/commands/design.ts +3 -3
  64. package/src/commands/docs.ts +1 -1
  65. package/src/commands/feedback.ts +2 -2
  66. package/src/commands/gate.ts +9 -2
  67. package/src/commands/gov.ts +2 -2
  68. package/src/commands/init.ts +1 -1
  69. package/src/commands/migrate.ts +1 -1
  70. package/src/commands/pr.ts +183 -7
  71. package/src/commands/sandbox.ts +10 -13
  72. package/src/commands/slides.ts +1 -1
  73. package/src/commands/sync.ts +1 -1
  74. package/src/commands/tasks.ts +19 -2
  75. package/src/commands/tooling.ts +1 -1
  76. package/src/commands/transcripts.ts +1 -1
  77. package/src/commands/upgrade.ts +1 -1
  78. package/src/commands/wiki.ts +1 -1
  79. package/src/context/architecture.ts +1 -1
  80. package/src/context/citations.ts +2 -2
  81. package/src/context/classify/extract.ts +1 -1
  82. package/src/context/folders.ts +2 -2
  83. package/src/context/requirements.ts +1 -1
  84. package/src/docs/read.ts +1 -1
  85. package/src/exec.ts +1 -1
  86. package/src/feedback/github.ts +1 -1
  87. package/src/gate/measures.ts +1 -1
  88. package/src/gate/sequencer.ts +55 -3
  89. package/src/gate/stages.ts +8 -0
  90. package/src/gate/suite-lock.ts +214 -0
  91. package/src/intake/folder.ts +1 -1
  92. package/src/labels/map.ts +1 -1
  93. package/src/labels/phase.ts +3 -3
  94. package/src/migrate/record-layout.ts +1 -1
  95. package/src/migrate/records.ts +11 -11
  96. package/src/migrate/scratch-evidence.ts +1 -1
  97. package/src/migrate/surface-roots.ts +6 -6
  98. package/src/pr/evidence.ts +154 -16
  99. package/src/pr/tick.ts +56 -0
  100. package/src/process/harness.ts +1 -1
  101. package/src/records/backup.ts +1 -1
  102. package/src/records/ordinal.ts +1 -1
  103. package/src/records/prune.ts +1 -1
  104. package/src/records/size.ts +1 -1
  105. package/src/records/stale-canonical.ts +1 -1
  106. package/src/records/stale.ts +1 -1
  107. package/src/records/validate.ts +2 -2
  108. package/src/{project-root.ts → roots/project.ts} +1 -1
  109. package/src/{record-root.ts → roots/record.ts} +1 -1
  110. package/src/sandbox/coverage.ts +11 -12
  111. package/src/sandbox/equivalence.ts +57 -26
  112. package/src/sandbox/hooks.ts +1 -1
  113. package/src/sandbox/provision.ts +78 -28
  114. package/src/sandbox/scenario.ts +588 -0
  115. package/src/sessions/live.ts +4 -1
  116. package/src/sessions/transfer/bundle.ts +1 -1
  117. package/src/slides/decks.ts +1 -1
  118. package/src/standards/read.ts +1 -1
  119. package/src/sync/engine.ts +1 -1
  120. package/src/sync/layout.ts +2 -2
  121. package/src/sync/stamp.ts +1 -1
  122. package/src/targets/validate.ts +1 -1
  123. package/src/tasks/answers.ts +1 -1
  124. package/src/tasks/archive.ts +1 -1
  125. package/src/tasks/label.ts +1 -1
  126. package/src/tasks/reach.ts +1 -1
  127. package/src/tasks/validate.ts +38 -14
  128. package/src/teach/nav.ts +1 -1
  129. package/src/teach/workspace.ts +1 -1
  130. package/src/tooling/inject.ts +1 -1
  131. package/src/tooling/read.ts +1 -1
  132. package/src/tooling/scan.ts +1 -1
  133. package/src/version/installed.ts +1 -1
  134. package/src/version/skew.ts +1 -1
  135. package/standards/board.md +3 -3
  136. package/standards/plan.md +13 -7
  137. package/tsconfig.json +1 -1
  138. /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
  139. /package/src/{surface-root.ts → roots/surface.ts} +0 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: The pull request checklist tick
3
+ description: How canon pr tick marks the checklist boxes a UI pass drove, the stamp that names the commit each passed at, why a render at a newer head clears a tick, the refusal reasons it names, and why a taste box is never ticked
4
+ ---
5
+
6
+ # The pull request checklist tick
7
+
8
+ `canon pr tick` ticks the boxes of the visual checklist on the pull request's
9
+ evidence comment. `review-ui` calls it once its verdict has posted, for every
10
+ box that passed at the head it drove, so the person merging reads a checklist
11
+ a driver worked through instead of one that stays empty.
12
+
13
+ ```bash
14
+ canon pr tick 1341 --boxes 1,2,4 --head <sha> --json
15
+ ```
16
+
17
+ Box numbers come from the `boxes` field of `canon pr evidence --json`, which
18
+ numbers the checklist's box lines in order. A caller never counts lines itself.
19
+
20
+ ## What a tick looks like
21
+
22
+ A ticked box ends in the commit it passed at, visible on the comment:
23
+
24
+ ```markdown
25
+ - [x] click "Pricing" in the header → the pricing page opens · passed at `<short-sha>`
26
+ ```
27
+
28
+ The stamp is visible rather than an HTML comment because the person it is for
29
+ reads the merge record, and a hidden head would tell the machine what the
30
+ reader cannot see. The reader matches the stamp only as the trailing segment of
31
+ a line, so a box whose own text says `passed at` is left alone.
32
+
33
+ ## When a tick is cleared
34
+
35
+ A tick is a claim about one commit, so a render at another commit does not
36
+ carry it forward. `canon pr evidence` settles the checklist before it renders:
37
+
38
+ - A tick stamped for the render's head stays.
39
+ - A tick stamped for any other head is cleared and loses its stamp.
40
+ - A tick with no stamp, such as one an author or the operator added by hand,
41
+ stays only when the comment it came from described the render's head.
42
+
43
+ A render at the same head after `--preview` keeps every tick. A push that
44
+ reaches the branch without a render leaves its ticks in place until the next
45
+ one, and the stamp still shows which head earned them.
46
+
47
+ ## What it refuses
48
+
49
+ `reason` on the record is what a caller branches on, not the exit code:
50
+
51
+ | Reason | What it means |
52
+ | -------------- | ------------------------------------------------------------------------------------------------------------ |
53
+ | `ticked` | The boxes were ticked, with the remote tip in `head`. |
54
+ | `stale-head` | `--head` is not the branch tip on the remote. Nothing is written. |
55
+ | `taste-box` | A named box ends in `(taste)`. Nothing is written. |
56
+ | `no-box` | A named number is past the end of the checklist. Nothing is written. |
57
+ | `no-checklist` | The evidence comment carries no checklist. |
58
+ | `no-comment` | No comment on the pull request carries the evidence marker. |
59
+ | `bad-boxes` | `--boxes` is not a list of positive whole numbers, or `--head` is not a hex sha of seven characters or more. |
60
+
61
+ A taste box is a judgment of look or feel that a driver reports as needing a
62
+ person's eyes. Refusing it in the verb makes that rule a check rather than a
63
+ sentence a driver can talk itself out of, and a refusal writes none of the
64
+ boxes named beside it.
65
+
66
+ The head is compared against the tip read from the remote rather than the pull
67
+ request object, which lags a push by up to a minute. A push landing between
68
+ that read and the write leaves a tick at an older head until the next render
69
+ clears it, the same window `canon pr head` documents.
70
+
71
+ The verb edits the marked comment in place with a `PATCH`, the way
72
+ `canon pr local --remove` does, and ticking a box again restamps it.
@@ -9,7 +9,7 @@ description: Scenario routing, the expectation scoring surface, and the coverage
9
9
 
10
10
  ## Scenarios
11
11
 
12
- Scenarios live under `sandbox/`, one folder per category. `sandbox/fixtures/` is the exception, holding file content that scenarios stage rather than scenarios of its own, so the picker and the coverage report both filter it out. That tree sits outside `files` in `package.json`, so an installed `canon` carries the command, reports it as toolkit-only on one line, and exits 1 rather than failing on the missing directory. Route non-interactively with `SANDBOX_SCENARIO`:
12
+ Scenarios live under `sandbox/`, one folder per category and one file per command, either a TypeScript module or a bash script, and provisioning refuses a command present in both forms. `sandbox/fixtures/` is the exception, holding file content that scenarios stage rather than scenarios of its own, so the picker and the coverage report both filter it out. That tree sits outside `files` in `package.json`, so an installed `canon` carries the command, reports it as toolkit-only on one line, and exits 1 rather than failing on the missing directory. Route non-interactively with `SANDBOX_SCENARIO`:
13
13
 
14
14
  ```bash
15
15
  SANDBOX_SCENARIO=sync canon sandbox infra:tooling
@@ -93,7 +93,7 @@ The baseline is a detached worktree with full history, removed on exit including
93
93
 
94
94
  The record lists per arm `identical`, `differs` with the manifest keys that differ, `red-on-base` when both sides fail the same way, or `skipped-anchor`. It also carries `canonVersion`, `baseCommit`, `masksApplied`, `errors`, and `counts`. A declared arm adds a `check` entry from `canon sandbox check` on the head tree, whose `asserted` count shows its `expect.toml` resolved from `sandbox/fixtures/`.
95
95
 
96
- Exit 0 means every compared arm is identical or red the same way on both sides. Exit 1 means any `differs`, a base and head exit that disagree, or an armed arm that `canon sandbox coverage` lists and enumeration lacks.
96
+ Exit 0 means every compared arm is identical or red the same way on both sides. Exit 1 means any `differs`, a base and head exit that disagree, an armed arm that `canon sandbox coverage` lists and enumeration lacks, a scenario present as both `.sh` and `.ts`, or a `.ts` scenario that fails to load.
97
97
 
98
98
  ## Scenario coverage
99
99
 
@@ -59,7 +59,7 @@ The question block is read through the same parser `canon tasks validate` runs,
59
59
 
60
60
  Branch on `launchable` rather than on the exit code, for the reason the section above states: a shell profile wrapping `canon` in a function can take a later command's status and mask every non-zero exit, which reads a waiting plan as a launchable one.
61
61
 
62
- The orchestrator dispatch runbook calls this before it checks the branch or the file sets, so a row whose plan still needs a person is handed back rather than launched into a worker that halts on the same question.
62
+ The orchestrator dispatch runbook calls this before it checks the branch or runs its conflict check, so a row whose plan still needs a person is handed back rather than launched into a worker that halts on the same question.
63
63
 
64
64
  ```bash
65
65
  canon tasks plan-answers dispatch-answer-gate --json | jq -r '.launchable'
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Tasks validate
3
- description: Validating the board against the tree, the seven checks, the untested, claims, wide, and unplaced arrays, the exit codes, and the refusal reasons
3
+ description: Validating the board against the tree, the seven checks, the five report arrays, the exit codes, and the refusal reasons
4
4
  ---
5
5
 
6
6
  # Tasks validate
@@ -12,17 +12,17 @@ canon tasks validate
12
12
  canon tasks validate --json
13
13
  ```
14
14
 
15
- Seven checks run. Collisions reaches one half of the `## Run now` test the board standard states, and Plan reaches every group, reading the Plan column under `## Run now` and the task file's own line under the other two. Mapping and Grouping test the folder contract and hold for every group, and Shape holds for every group too, ahead of the four. Ordering reaches only the `## Needs a plan` rows, and Blockers reaches every row outside `## Run now`:
15
+ Seven checks run. Touches reaches only the `## Run now` rows, asking each to name its files, and Plan reaches every group, reading the Plan column under `## Run now` and the task file's own line under the other two. Mapping and Grouping test the folder contract and hold for every group, and Shape holds for every group too, ahead of the four. Ordering reaches only the `## Needs a plan` rows, and Blockers reaches every row outside `## Run now`:
16
16
 
17
- | Check | What it reports |
18
- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
19
- | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
20
- | Plan | A row whose plan citation disagrees with its group or its task file, or a task whose `Plan:` line lists one plan twice |
21
- | Mapping | A row or backlog line naming no task file |
22
- | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
23
- | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
24
- | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
25
- | Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
17
+ | Check | What it reports |
18
+ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
19
+ | Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
20
+ | Plan | A row whose plan citation disagrees with its group or its task file, or a task whose `Plan:` line lists one plan twice |
21
+ | Mapping | A row or backlog line naming no task file |
22
+ | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
23
+ | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
24
+ | Touches | A `## Run now` row whose Touches column names no file |
25
+ | Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
26
26
 
27
27
  Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
28
28
 
@@ -42,9 +42,9 @@ The ordinal phrase is prose rather than data, searched for anywhere in the cell
42
42
 
43
43
  The comparative phrase is bounded the same way and for the same reason. A closed verb list of `leads`, `heads`, `opens`, `closes`, `trails`, `precedes`, `follows`, `outranks`, and `sits under`, `above`, or `below` has to sit in one clause with a positional object, which is a `vNN.N` phase label, the word `group`, or `row` or `rows`. Both halves are needed, since a cell reading `it closes a gap the reference gate leaves open` carries the verb and claims no position, and the clause bound is what stops a verb in one half of the cell pairing with an object in the other. An ordinal exempts the row, being a comparative claim already. An unusual comparative phrasing reports as unranked, the safe direction for a check over prose, and a vocabulary verb reaching a positional object non-positionally reports as ranked, which only grading prose could separate.
44
44
 
45
- The collision check is the one a person cannot run by eye. Paths come from the backticked spans in the Touches column, a span naming no file is dropped, and a directory collides with any file beneath it. A `## Run now` row whose column parses to nothing is reported rather than skipped, since a row stating no file set makes a claim nothing can check.
45
+ The Touches check is the one a person cannot run by eye. Paths come from the backticked spans in the Touches column, a span naming no file is dropped, and a directory shares any file beneath it. A `## Run now` row whose column parses to nothing is `touches-unstated`, since a row stating no file set makes a claim nothing can check.
46
46
 
47
- Where a directory holds the other row's file, the finding names the row that claimed it, reading `both touch src/tasks, which v2.0-second claims as a folder.` The shared strings alone leave an over-broad cell and a genuine overlap identical. <!-- canon-allow-reference: illustrates the finding's own sentence shape, not a citation of a real task -->
47
+ Two `## Run now` rows naming a path in common land in a second array, `shared`, which moves no exit code since the branch merging second rebases. Merge order reads it, as does a dispatcher weighing a contract, relocation, or sweep hold. Where a directory holds the other row's file, the entry names the row that claimed it, reading `both touch src/tasks, which v2.0-second claims as a folder.` The shared strings alone leave an over-broad cell and a genuine overlap identical. <!-- canon-allow-reference: illustrates the entry's own sentence shape, not a citation of a real task -->
48
48
 
49
49
  The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or by its work reaching the trunk, and a collision is settled by nothing under `## Run now` still holding the file the cell cites. A cited task resolving under `.canon/tasks/declined/` instead reports separately as `blocker-declined`, since a decided-against task is neither the shipped work `blocker-settled` reports nor the dangling pointer `blocker-unresolved` reports.
50
50
 
@@ -69,7 +69,7 @@ Both halves gate on a citation inside the `Waiting on` cell, never on the column
69
69
 
70
70
  A citation resolving in none of the board, the archive, or the declined folder is `blocker-unresolved` rather than a settled row. Reading an absent file as archived or declined states a specific fact about a file nobody ever wrote, which is what a renamed task or a typo produces, and only a task that genuinely closed or was genuinely declined releases the row waiting on it.
71
71
 
72
- The other three kinds rest on a person's judgment, so a row neither half reached lands in a second array rather than in the findings:
72
+ The other three kinds rest on a person's judgment, so a row neither half reached lands in a third array rather than in the findings:
73
73
 
74
74
  ```json
75
75
  {
@@ -85,7 +85,7 @@ The other three kinds rest on a person's judgment, so a row neither half reached
85
85
 
86
86
  An untested row is not a finding and moves no exit code. Reading a clean findings list as a clean board is the failure the array exists to prevent, and `orchestrator-parked.md` is the pass that takes those rows by hand.
87
87
 
88
- A `## Run now` row whose Touches column names a bare folder lands in a third array on the same reasoning:
88
+ A `## Run now` row whose Touches column names a bare folder lands in a fourth array on the same reasoning:
89
89
 
90
90
  ```json
91
91
  {
@@ -101,11 +101,11 @@ A `## Run now` row whose Touches column names a bare folder lands in a third arr
101
101
 
102
102
  That claim collides with every row a later session writes under the folder, and it is legitimate whenever the row does rewrite the directory, so the array states the reach and moves no exit code. A measure failing on a cell that is right teaches a reader to skip it. Folder against file is decided by asking the tree for a path that resolves, and by the extension only for a path the row has yet to create, since the name alone reads a file carrying no extension as a folder.
103
103
 
104
- The scan reaches `## Run now` and stops, where the collision check stops. A cell in another group describes work nobody has planned, written as a sentence and rewritten once a plan exists, so a claim read off one reports on prose rather than on a file set. A parked folder claim surfaces when its row is promoted, which is when the cell becomes something a dispatcher can act on.
104
+ The scan reaches `## Run now` and stops, where the Touches check stops. A cell in another group describes work nobody has planned, written as a sentence and rewritten once a plan exists, so a claim read off one reports on prose rather than on a file set. A parked folder claim surfaces when its row is promoted, which is when the cell becomes something a dispatcher can act on.
105
105
 
106
- A code span in any cell that the preview cannot break lands in a fourth array, `wide`, with the same `group`, `subject`, and `message` fields. The scan measures a span's longest whitespace-free run and flags one past 60 characters, or a brace-glob at any width. A 57-character span fit the pane and a 78-character one did not, so the limit is a heuristic and the array moves no exit code. A link target is skipped, since only its label renders, and the scan reaches every group, since the overflow it exists for sat under `## Up next`.
106
+ A code span in any cell that the preview cannot break lands in a fifth array, `wide`, with the same `group`, `subject`, and `message` fields. The scan measures a span's longest whitespace-free run and flags one past 60 characters, or a brace-glob at any width. A 57-character span fit the pane and a 78-character one did not, so the limit is a heuristic and the array moves no exit code. A link target is skipped, since only its label renders, and the scan reaches every group, since the overflow it exists for sat under `## Up next`.
107
107
 
108
- A task file neither surface names lands in a fifth array, on the same reasoning:
108
+ A task file neither surface names lands in a sixth array, on the same reasoning:
109
109
 
110
110
  ```json
111
111
  {
@@ -118,6 +118,6 @@ canon tasks list --json | jq -r '.tasks[] | select(.readiness == "backlog") | .s
118
118
 
119
119
  ## Validate
120
120
 
121
- `canon tasks validate` reports what each row of `priority.md` and each line of `backlog.md` claims against what the tree holds. Its seven checks, the four arrays that move no exit code, and the refusals are in `tasks-validate.md`.
121
+ `canon tasks validate` reports what each row of `priority.md` and each line of `backlog.md` claims against what the tree holds. Its seven checks, the five arrays that move no exit code, and the refusals are in `tasks-validate.md`.
122
122
 
123
123
  For the board format, see `standards/board.md`. For the `Pull request:` line and the archive rules, see `standards/tasks.md`.
@@ -34,7 +34,7 @@ it. All four are framing and boundaries rather than logic.
34
34
 
35
35
  The planner is the one role the orchestrator also performs. Per-row planning
36
36
  runs warm inside the orchestrator's own session or cold in a dispatched one, and
37
- the boundary between them is the cross-feature call: which rows collide, what
37
+ the boundary between them is the cross-feature call: which rows conflict, what
38
38
  merges before what, and whether a row should run at all stay with the
39
39
  orchestrator, because a session reading the board sees blockers and file sets
40
40
  and can write a confident merge order off a partial picture. The per-row
@@ -62,20 +62,20 @@ Filing that row stops at the defect and the surface it was seen on. How far the
62
62
  defect reaches and what causes it belong to whoever plans the row, so it arrives
63
63
  carrying neither a count nor an asserted mechanism, and the seat that found it
64
64
  says as much in a plain sentence rather than supplying either. What separates the
65
- two is what a measurement decides: the blocker re-test, the collision check, and
66
- the file set compared against every track in flight all decide whether a row can
65
+ two is what a measurement decides: the blocker re-test, the branch check, and
66
+ the conflict check against every track in flight all decide whether a row can
67
67
  start, and only a count of the defect's extent decides how big it is.
68
68
 
69
69
  ## The loop
70
70
 
71
71
  One feature travels this path end to end.
72
72
 
73
- 1. The next feature is planned with `plan-feature`, writing a plan to `.canon/plans/`. The orchestrator runs it warm when the row turns on a contract other features consume or a shared wiring seam, and dispatches a planner under `role-planner` otherwise. A cold planner measures the row against the tree rather than trusting what the row claims, and it reads what is in flight by composing the live session roster with open pull requests. A bare branch or worktree is not evidence on its own, since this repository leaves both behind after a squash merge.
74
- 2. Orchestrator checks the plan waits on nobody, checks the branch is unclaimed, and checks the plan's file set is disjoint from every track in flight, then dispatches a background worker with `claude --bg` against the plan, naming the branch and the model on the launch rather than leaving the worker to derive either. No count caps how many run at once. The branch travels as the argument to the worker's own worktree call, which is the one place the name is read rather than inferred. It falls back to naming the invocation for a human to run through `session-worktree` and `auto-ship` when the plan still waits on an answer only the operator can give, the check refuses, the sets overlap, or a stated reason serializes the plan behind a track already in flight. Either way, the worker enters its own worktree, builds, self-checks, opens a PR, and stops at the PR boundary.
73
+ 1. The next feature is planned with `plan-feature`, writing a plan to `.canon/plans/`. The orchestrator runs it warm when the row turns on a contract other features consume or a dependency on another row, and dispatches a planner under `role-planner` otherwise. A cold planner measures the row against the tree rather than trusting what the row claims, and it reads what is in flight by composing the live session roster with open pull requests. A bare branch or worktree is not evidence on its own, since this repository leaves both behind after a squash merge.
74
+ 2. Orchestrator checks the plan waits on nobody, checks the branch is unclaimed, and checks no conflict holds the plan behind a track in flight, then dispatches a background worker with `claude --bg` against the plan, naming the branch and the model on the launch rather than leaving the worker to derive either. No count caps how many run at once. The branch travels as the argument to the worker's own worktree call, which is the one place the name is read rather than inferred. It falls back to naming the invocation for a human to run through `session-worktree` and `auto-ship` when the plan still waits on an answer only the operator can give, the branch check refuses, or a conflict holds the plan behind a track already in flight. Either way, the worker enters its own worktree, builds, self-checks, opens a PR, and stops at the PR boundary.
75
75
  3. A reviewer under `role-reviewer`, or the orchestrator, posts `review-pr` findings, and `review-ui` drives any visual checklist beside it.
76
- 4. Orchestrator tells the session holding that branch to run `review-address` once the pass posted a finding at any severity, resolving the target then with `canon sessions list --branch` and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first and left the branch unable to merge, then pushes a follow-up. A pass carrying only minor findings dispatches too, since the grade runs low often enough that a floor at should-fix loses fixes a worker would have made. `review-pr` states that threshold and the heading follows it, so an open heading is itself the signal to send.
76
+ 4. Orchestrator tells the session holding that branch to run `review-address` once the pass posted a finding at any severity, resolving the target then with `canon sessions list --branch` and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first on a file it writes, then pushes a follow-up. A pass carrying only minor findings dispatches too, since the grade runs low often enough that a floor at should-fix loses fixes a worker would have made. `review-pr` states that threshold and the heading follows it, so an open heading is itself the signal to send.
77
77
  5. Orchestrator closes the review out with `review-pr` again. The second pass reads only the commits the follow-up added, or the worker's response alone when the follow-up added none, and posts under `## Review` when it finds anything and under `## Review closed` when it finds nothing, so a reader learns from the heading whether work is still owed and takes the merge decision from the counts on the line under it. A pass finding nothing where a close-out already stands rewrites that comment to cover what it read rather than posting a second one, so the thread carries one live verdict. Repeat from step 4 until a pass closes the review.
78
- 6. The human reads the result and merges. The orchestrator tells any trailing worker whose branch shares a seam with the merged one to run `review-address`, which rebases whether or not the review left anything open.
78
+ 6. The human reads the result and merges. After each merge, the orchestrator tells any trailing worker whose files the merged diff touches to run `review-address`, which rebases whether or not the review left anything open.
79
79
 
80
80
  There is no loop construct here. Each worker is a single build that halts at the
81
81
  PR. The merge stays a manual human gate. Reliability comes from the plan being
@@ -148,8 +148,8 @@ whether the branch is blocked, so the pass reads the worker's response rather th
148
148
  a delta and names its body from that response instead of from a tree that did not
149
149
  change.
150
150
 
151
- A branch that stopped merging while the review was open is the worker's problem
152
- to close. `review-address` rebases onto `origin/main` between the fixes
151
+ A branch `main` moved under during review, conflicting or not, is the worker's
152
+ problem to close. `review-address` rebases onto `origin/main` between the fixes
153
153
  and the push, so one force-push carries both and the reviewer reads one delta.
154
154
  The staleness test sits ahead of the no-findings guard, so a branch whose review
155
155
  closed clean and then went stale still rebases when the skill is invoked. The
@@ -200,12 +200,7 @@ before another is carried nowhere at all.
200
200
 
201
201
  ## Parallelism
202
202
 
203
- The binding constraint is the human and the shared files rather than the board.
204
- No number caps worker tracks. Open one whenever its file set is disjoint from
205
- every track already in flight, compared at the file path rather than at a folder
206
- above it, and stop adding once you can no longer review every output properly.
207
- Serialize a track sharing a wiring seam with another, and serialize one whose
208
- sets are disjoint when a stated reason still puts it behind another.
203
+ Review attention binds, and no number caps worker tracks, so stop adding once you cannot review every output. A row waits only on a conflict a merge cannot settle, which the `role-orchestrator` dispatch runbook states once. A shared file is not one: both rows build, and the branch merging second resolves any conflict in its own session after the first lands, since no practice source serializes on a file. [Fowler](https://martinfowler.com/articles/branching-patterns.html) has whoever integrates second check health "even if it's a clean merge", and [DORA](https://dora.dev/capabilities/trunk-based-development/) keeps branches to hours with no integration phase.
209
204
 
210
205
  Review splits by vantage. A reviewer under `role-reviewer` takes a pull
211
206
  request's read, first pass included, when three await one (a number set by
@@ -24,7 +24,7 @@ Execution order comes off `.canon/tasks/priority.md` and nothing sequences work
24
24
 
25
25
  Run one orchestrator at a time. The board is gitignored, so a second session reads none of the first one's writes and the two collide on labels and archives.
26
26
 
27
- See [operating model](operating-model.md) for what caps the worker tracks under it and when a disjoint one still waits. An operator caps a session's workers by saying so, and that cap binds for the session rather than standing as a number in a file.
27
+ See [operating model](operating-model.md) for what caps the worker tracks under it and what conflict holds one behind another. An operator caps a session's workers by saying so, and that cap binds for the session rather than standing as a number in a file.
28
28
 
29
29
  Before a handoff, the orchestrator checks the plan against the tree rather than reading it: grep each construct it names and count the sites, confirm every phase label it cites is still open, and open each file it describes. A plan goes stale from whatever merged after it was written, and reading cannot catch that.
30
30
 
@@ -60,22 +60,22 @@ A branch review report takes the other route and is swept rather than archived.
60
60
 
61
61
  A fan-out of N worktrees produces N pull requests. The order they merge in matters only when two branches touch the same file.
62
62
 
63
- **Collision profile.** Work confined to one domain directory, such as a new skill under its own folder or independent fixes in different subtrees of `scripts/`, rarely collides. These paths are shared hotspots, so a branch touching one serializes rather than fans out, because parallel edits almost always produce a conflict or a stale regenerated file:
63
+ **Rebase profile.** Work confined to one domain directory, such as a new skill under its own folder or independent fixes in different subtrees of `scripts/`, rarely touches a sibling's files. A shared file does not hold a branch back, per the conflict test in [operating model](operating-model.md). These paths are shared hotspots, the ones a second branch should expect to rebase over and resolve:
64
64
 
65
65
  - `CLAUDE.md`, since cross-domain behavior lives there
66
66
  - `tooling/**`, since stack manifests, golden configs, and seeds are tightly coupled
67
67
  - `canon/context/context-model/overview.md` and `docs/agents/`, which several domains cross-reference
68
- - Any folder's `index.md`, since `canon indexes regen` rewrites it and two worktrees adding files to a regen-covered folder race on it
68
+ - Any folder's `index.md`, since `canon indexes regen` rewrites it and two worktrees adding files to a regen-covered folder race on it. Resolve its conflict by running the regen over the rebased tree rather than by hand.
69
69
 
70
- Land any in-flight edit to a hotspot on `main` before fanning out. A worktree on a stale `CLAUDE.md` costs more to rebase than it saved.
70
+ Land any in-flight edit to a hotspot on `main` before fanning out where you can. A worktree on a stale `CLAUDE.md` costs more to rebase than it saved.
71
71
 
72
72
  **Merge order.** Merge the branch with the smallest hotspot footprint first. A branch touching `CLAUDE.md`, a context entry, or a regenerated `index.md` merges last, and its siblings rebase on the new `main` once it lands.
73
73
 
74
- **Rebase before the next merge.** After one squash-merge, each sibling is behind `main` and may carry a stale copy of a shared file. Run `git fetch origin && git rebase origin/main` in the sibling, resolve any conflict in the worktree, push, then merge. Never force-merge a stale branch.
74
+ **Rebase before the next merge.** After one squash-merge, each sibling is behind `main` and may carry a stale copy of a shared file. The branch merging second resolves, in the session that built it and after the first lands, never against a sibling still building: run `/review-address` there, which trial-merges against `origin/main` and rebases a branch that no longer merges or whose files `main` changed. Push, then merge. Never force-merge a stale branch.
75
75
 
76
76
  **Clean up after merge.** Run `/git-worktree cleanup` to remove worktrees whose branches merged on GitHub and prune the local branches. The skill reads merge state through `gh pr view`. To start a fresh feature from a stale worktree, `ExitWorktree(action: "keep")` back to main, then `/session-worktree <new-name>`.
77
77
 
78
- **When fan-out was the wrong call.** If every sibling needs a rebase and every rebase conflicts on the same file, the branches should have serialized. Land one, wait, then start the next. The cleanup skill only tidies a flow that already worked.
78
+ **When fan-out was the wrong call.** Sharing a file was never the reason. If every rebase needs a decision the tree does not carry, because one branch depended on another, changed a contract the other consumed, moved a path the other wrote, or swept a folder the other wrote inside, the branches should have serialized. Land one, wait, then start the next. The cleanup skill only tidies a flow that already worked.
79
79
 
80
80
  ## The task board
81
81
 
@@ -85,7 +85,7 @@ Land any in-flight edit to a hotspot on `main` before fanning out. A worktree on
85
85
 
86
86
  `canon tasks validate` reads a row against its own table before it reads anything the row claims. A blank or prose line closes the table above it, so a row stranded there is checked against the line behind it rather than parsed as a continuation, and a row that clears that test still has its cell count checked against its header. A `## Needs a plan` row that states its own position, searched for `<ordinal> here` or the bare word `last` anywhere in the cell rather than at its start, is checked against where it actually sits, which is what catches a gap, a duplicate, and a sequence starting somewhere other than first alike.
87
87
 
88
- Past that shape, it checks what a surviving row claims against what the tree holds: every plan pointer resolves, every task file is named by a board row or a backlog line and never by both, no task sits in two groups, and no two rows marked ready touch the same file. One check across both surfaces is what lets a task move between the board and the backlog without the move reading as a dropped file. The collision check is the half a reader cannot run by eye, and it is what keeps two workers from being handed colliding work. Blockers re-takes what a parked row waits on, reporting one whose cited task reached the trunk and one whose cited file nothing running still holds. A cited task settles the row by being archived, or by closing every outcome and naming a pull request the trunk carries, since the checkbox alone is marked while the branch is still in review. Both halves read a citation out of the blocker cell, so a row citing neither is reported as untested rather than counted clean, and so is a cited task the trunk could not answer for. It reports and never writes, because a row is the orchestrator's claim and a validator repairing one would assert the claim it exists to test. Nothing fires it automatically, since the board is gitignored per-machine scratch with no shared moment to hang a hook on, so the orchestrator's sweep calls it at the point the readiness claim is made and follows it with the parked re-test.
88
+ Past that shape, it checks what a surviving row claims against what the tree holds: every plan pointer resolves, every task file is named by a board row or a backlog line and never by both, no task sits in two groups, and every row marked ready names its files. One check across both surfaces is what lets a task move between the board and the backlog without the move reading as a dropped file. Paths two ready rows share go to a `shared` array moving no exit code, the candidate list the orchestrator's conflict check reads. Blockers re-takes what a parked row waits on, reporting one whose cited task reached the trunk and one whose cited file nothing running still holds. A cited task settles the row by being archived, or by closing every outcome and naming a pull request the trunk carries, since the checkbox alone is marked while the branch is still in review. Both halves read a citation out of the blocker cell, so a row citing neither is reported as untested rather than counted clean, and so is a cited task the trunk could not answer for. It reports and never writes, because a row is the orchestrator's claim and a validator repairing one would assert the claim it exists to test. Nothing fires it automatically, since the board is gitignored per-machine scratch with no shared moment to hang a hook on, so the orchestrator's sweep calls it at the point the readiness claim is made and follows it with the parked re-test.
89
89
 
90
90
  `canon:task-board` owns the two operations that bracket a task's life. It creates the file, holding the filename convention and the frontmatter contract so a malformed write cannot break the index for every sibling, and it moves a shipped task to `.canon/tasks/archive/`. Creation is where the origin invariant is enforced: every task names a plan, a groundwork folder, an intake folder, or an issue, since a task with no origin is either lost context or work nobody decided to do.
91
91
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.27.0",
4
+ "version": "5.28.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": {
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
2
  import { dirname, join, relative } from 'node:path'
3
3
  import type { AuditResult } from '@/audits/catalog'
4
- import { resolveExisting } from '@/legacy-path'
4
+ import { resolveExisting } from '@/roots/legacy'
5
5
 
6
6
  /**
7
7
  * Where the retained counts live, relative to the project root.
package/src/audits/run.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type Corpus,
9
9
  } from '@/audits/catalog'
10
10
  import { gitEnv } from '@/git/env'
11
- import { PROJECT_ROOT } from '@/project-root'
11
+ import { PROJECT_ROOT } from '@/roots/project'
12
12
 
13
13
  /** Every value a corpus filter accepts, matching the `Corpus` union. */
14
14
  export const CORPORA: readonly Corpus[] = ['tracked', 'per-machine', 'upstream']
@@ -13,7 +13,7 @@ import {
13
13
  FRAMES_PREFIX,
14
14
  startCanvas,
15
15
  } from '@/canvas/server'
16
- import { recordDir } from '@/record-root'
16
+ import { recordDir } from '@/roots/record'
17
17
 
18
18
  /**
19
19
  * The element a frame is captured by. The document rather than its body, so a
@@ -21,6 +21,8 @@ export interface Scrub {
21
21
  readonly begin: () => ScrubSession
22
22
  /** Holds a scrubbed number inside what the property accepts. */
23
23
  readonly clamp: (value: number) => number
24
+ /** Scales each pixel's move, for a value whose useful range is small. */
25
+ readonly step?: number
24
26
  }
25
27
 
26
28
  interface FieldProps {
@@ -120,8 +122,11 @@ export function Field({
120
122
  const moved = event.clientX - held.lastX
121
123
  held.lastX = event.clientX
122
124
  held.value = scrub.clamp(
123
- Math.round((held.value + moved * scrubStep(event.shiftKey)) * 10) /
124
- 10,
125
+ Math.round(
126
+ (held.value +
127
+ moved * scrubStep(event.shiftKey) * (scrub.step ?? 1)) *
128
+ 10,
129
+ ) / 10,
125
130
  )
126
131
  const shown = String(held.value)
127
132
  if (input.current) input.current.value = shown
@@ -177,6 +182,60 @@ export function Field({
177
182
  )
178
183
  }
179
184
 
185
+ interface SelectFieldProps {
186
+ readonly label: string
187
+ readonly glyph: string
188
+ /** Each option's value, which is also what it shows. */
189
+ readonly options: readonly string[]
190
+ readonly value: string
191
+ readonly isBusy: boolean
192
+ readonly isWide?: boolean
193
+ readonly onCommit: (value: string) => void
194
+ }
195
+
196
+ /**
197
+ * A value picked from a fixed list, drawn like a field with a chevron. The
198
+ * select is native, so its list opens above the panel's clipping. A current
199
+ * value outside the list stays as an option, so it shows rather than blanks.
200
+ */
201
+ export function SelectField({
202
+ label,
203
+ glyph,
204
+ options,
205
+ value,
206
+ isBusy,
207
+ isWide,
208
+ onCommit,
209
+ }: SelectFieldProps): JSX.Element {
210
+ const shown = options.includes(value) ? options : [...options, value]
211
+ return (
212
+ <div class={fieldClass(isWide, 'is-select')}>
213
+ <span class="glyph" aria-hidden="true" title={label}>
214
+ {glyph}
215
+ </span>
216
+ <select
217
+ class="glyph-field-input glyph-select"
218
+ aria-label={label}
219
+ value={value}
220
+ disabled={isBusy}
221
+ onChange={(event) => {
222
+ const picked = event.currentTarget.value
223
+ if (picked !== value) onCommit(picked)
224
+ }}
225
+ >
226
+ {shown.map((option) => (
227
+ <option key={option} value={option}>
228
+ {option}
229
+ </option>
230
+ ))}
231
+ </select>
232
+ <svg class="select-chevron" viewBox="0 0 16 16" aria-hidden="true">
233
+ <path d="M4.5 6.5 8 10l3.5-3.5" />
234
+ </svg>
235
+ </div>
236
+ )
237
+ }
238
+
180
239
  interface ReadOnlyFieldProps {
181
240
  readonly label: string
182
241
  readonly glyph: string
@@ -679,3 +679,81 @@
679
679
  height: 14px;
680
680
  fill: currentColor;
681
681
  }
682
+
683
+ /*
684
+ * A value picked from a fixed list. The native select draws its value as the
685
+ * field's text and the chevron sits over its right edge, so the platform list
686
+ * opens on press and the field keeps the height of every other one.
687
+ */
688
+ .glyph-field.is-select {
689
+ position: relative;
690
+ }
691
+
692
+ .glyph-select {
693
+ appearance: none;
694
+ padding-right: 22px;
695
+ cursor: pointer;
696
+ }
697
+
698
+ .select-chevron {
699
+ position: absolute;
700
+ right: 6px;
701
+ width: 12px;
702
+ height: 12px;
703
+ fill: none;
704
+ stroke: var(--color-muted);
705
+ stroke-width: 1.5;
706
+ pointer-events: none;
707
+ }
708
+
709
+ /* A raw marker inside a field sits before its menu, clear of the value. */
710
+ .glyph-field .raw {
711
+ padding: 0 2px 0 var(--space-xs);
712
+ }
713
+
714
+ /* Mutually exclusive choices for one value, drawn as one field split evenly. */
715
+ .segmented {
716
+ display: grid;
717
+ grid-template-columns: repeat(4, minmax(0, 1fr));
718
+ gap: 2px;
719
+ min-height: 24px;
720
+ padding: 1px;
721
+ border: 1px solid var(--color-border);
722
+ border-radius: var(--radius-action);
723
+ background: var(--color-surface);
724
+ }
725
+
726
+ .segment {
727
+ display: grid;
728
+ place-items: center;
729
+ min-height: 22px;
730
+ padding: 0;
731
+ border: 0;
732
+ border-radius: var(--radius-action);
733
+ background: transparent;
734
+ color: var(--color-muted);
735
+ cursor: pointer;
736
+ }
737
+
738
+ .segment:hover {
739
+ color: var(--color-text);
740
+ }
741
+
742
+ .segment[aria-pressed='true'] {
743
+ background: var(--color-chrome);
744
+ color: var(--color-text);
745
+ }
746
+
747
+ .segment:disabled {
748
+ opacity: 0.5;
749
+ cursor: not-allowed;
750
+ }
751
+
752
+ .segment svg {
753
+ width: 14px;
754
+ height: 14px;
755
+ fill: none;
756
+ stroke: currentColor;
757
+ stroke-width: 1.5;
758
+ stroke-linecap: round;
759
+ }