@erclx/canon 5.42.0 → 5.44.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 (195) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +3 -3
  3. package/claude/skills/backlog-triage/references/apply.md +6 -6
  4. package/claude/skills/canon-cli/SKILL.md +4 -4
  5. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
  8. package/claude/skills/ci-workflow/SKILL.md +12 -2
  9. package/claude/skills/ci-workflow/references/workflows.md +12 -9
  10. package/claude/skills/context-fold/references/context.md +1 -1
  11. package/claude/skills/create-rule/SKILL.md +1 -0
  12. package/claude/skills/create-skill/REQUIREMENT.md +1 -1
  13. package/claude/skills/create-skill/SKILL.md +2 -2
  14. package/claude/skills/create-skill/references/skill-requirement.md +68 -9
  15. package/claude/skills/create-skill/references/skill.md +1 -1
  16. package/claude/skills/decision-escalate/SKILL.md +1 -2
  17. package/claude/skills/design-identity/REQUIREMENT.md +40 -0
  18. package/claude/skills/design-identity/SKILL.md +138 -0
  19. package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
  20. package/claude/skills/design-taste/REQUIREMENT.md +4 -4
  21. package/claude/skills/design-taste/SKILL.md +2 -2
  22. package/claude/skills/design-taste/references/kinds.md +1 -1
  23. package/claude/skills/design-taste/references/preflight.md +1 -1
  24. package/claude/skills/document-health/references/axes.md +2 -1
  25. package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
  26. package/claude/skills/draft-and-pick/SKILL.md +3 -153
  27. package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
  28. package/claude/skills/draft-doc/SKILL.md +4 -76
  29. package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
  30. package/claude/skills/draft-figure/SKILL.md +3 -77
  31. package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
  32. package/claude/skills/draft-identity/SKILL.md +3 -130
  33. package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
  34. package/claude/skills/draft-ready/SKILL.md +3 -128
  35. package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
  36. package/claude/skills/draft-screencast/SKILL.md +3 -62
  37. package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
  38. package/claude/skills/draft-slides/SKILL.md +3 -72
  39. package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
  40. package/claude/skills/draw-figure/SKILL.md +85 -0
  41. package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
  42. package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
  43. package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
  44. package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
  45. package/claude/skills/git-commit/references/commit.md +1 -5
  46. package/claude/skills/git-pr/references/pr.md +1 -0
  47. package/claude/skills/make-deck/REQUIREMENT.md +44 -0
  48. package/claude/skills/make-deck/SKILL.md +80 -0
  49. package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
  50. package/claude/skills/markdown-craft/SKILL.md +2 -2
  51. package/claude/skills/pick-by-eye/REQUIREMENT.md +86 -0
  52. package/claude/skills/pick-by-eye/SKILL.md +161 -0
  53. package/claude/skills/plan-feature/SKILL.md +3 -2
  54. package/claude/skills/plan-feature/references/plan.md +25 -11
  55. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  56. package/claude/skills/plan-intake/SKILL.md +1 -1
  57. package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
  58. package/claude/skills/plan-ready/SKILL.md +136 -0
  59. package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
  60. package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
  61. package/claude/skills/pr-evidence/references/evidence.md +1 -1
  62. package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
  63. package/claude/skills/record-screencast/SKILL.md +2 -2
  64. package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
  65. package/claude/skills/role-orchestrator/SKILL.md +2 -0
  66. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
  67. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -0
  68. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  69. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
  70. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  71. package/claude/skills/role-planner/SKILL.md +5 -4
  72. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  73. package/claude/skills/role-worker/SKILL.md +35 -2
  74. package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
  75. package/claude/skills/script-screencast/SKILL.md +70 -0
  76. package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
  77. package/claude/skills/search-craft/REQUIREMENT.md +4 -4
  78. package/claude/skills/search-craft/SKILL.md +2 -2
  79. package/claude/skills/target-setup/SKILL.md +1 -0
  80. package/claude/skills/task-board/SKILL.md +11 -9
  81. package/claude/skills/task-board/references/board.md +15 -12
  82. package/claude/skills/teach-workspace/SKILL.md +1 -1
  83. package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
  84. package/claude/skills/ui-checklist/SKILL.md +2 -0
  85. package/claude/skills/write-doc/REQUIREMENT.md +46 -0
  86. package/claude/skills/write-doc/SKILL.md +83 -0
  87. package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
  88. package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
  89. package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
  90. package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +4 -4
  91. package/claude/skills/{draft-doc → write-doc}/references/wiki.md +1 -1
  92. package/docs/agents/audits.md +1 -0
  93. package/docs/agents/markdown-audit.md +4 -2
  94. package/docs/agents/migrate.md +13 -10
  95. package/docs/target/migrations.md +28 -4
  96. package/docs/target/projects.md +1 -1
  97. package/docs/workflow/parallel-features.md +3 -2
  98. package/docs/workflow/skill-map-on-request.md +18 -12
  99. package/docs/workflow/skill-map.md +13 -12
  100. package/docs/workflow/visual-design-workflow.md +2 -2
  101. package/governance/detect.toml +4 -0
  102. package/governance/ledger/framework/astro.md +73 -0
  103. package/governance/ledger/framework/fastapi.md +76 -0
  104. package/governance/ledger/framework/nextjs.md +86 -0
  105. package/governance/ledger/framework/react.md +83 -0
  106. package/governance/ledger/framework/shadcn.md +60 -0
  107. package/governance/ledger/framework/tailwind.md +55 -0
  108. package/governance/rules/canon/context.md +2 -1
  109. package/governance/rules/canon/plan.md +3 -5
  110. package/governance/rules/canon/scratch.md +3 -1
  111. package/governance/rules/canon/task-scope.md +2 -1
  112. package/governance/rules/canon/tasks.md +2 -1
  113. package/governance/rules/canon/worktrees.md +5 -2
  114. package/governance/rules/claude/output.md +7 -4
  115. package/governance/rules/claude/skill.md +6 -4
  116. package/governance/rules/framework/astro.md +12 -14
  117. package/governance/rules/framework/fastapi.md +20 -24
  118. package/governance/rules/framework/nextjs.md +19 -27
  119. package/governance/rules/framework/react.md +22 -33
  120. package/governance/rules/framework/shadcn.md +11 -15
  121. package/governance/rules/framework/tailwind.md +12 -14
  122. package/governance/rules/tooling/ci-workflow.md +6 -39
  123. package/governance/rules/ui/design-reference.md +37 -0
  124. package/governance/rules/writing/markdown.md +3 -2
  125. package/package.json +6 -6
  126. package/src/audits/catalog.ts +42 -0
  127. package/src/claude/cases/authoring.ts +15 -3
  128. package/src/claude/cases/misc.ts +12 -0
  129. package/src/claude/cases/workflow.ts +19 -1
  130. package/src/claude/settings.ts +8 -4
  131. package/src/claude/skills-audit.ts +113 -1
  132. package/src/commands/claude.ts +45 -12
  133. package/src/commands/demo.ts +1 -1
  134. package/src/commands/gov.ts +198 -0
  135. package/src/commands/markdown.ts +1 -1
  136. package/src/commands/migrate.ts +125 -2
  137. package/src/commands/pr.ts +34 -83
  138. package/src/commands/tasks.ts +383 -2
  139. package/src/commands/tooling.ts +3 -3
  140. package/src/commands/worktrees.ts +49 -0
  141. package/src/context/audit.ts +1 -1
  142. package/src/demo/beats.ts +1 -1
  143. package/src/gate/stages.ts +15 -0
  144. package/src/gov/audit.ts +441 -0
  145. package/src/listeners/belongs.ts +21 -0
  146. package/src/listeners/machine.ts +93 -0
  147. package/src/listeners/parse.ts +41 -0
  148. package/src/markdown/scope.ts +45 -35
  149. package/src/migrate/record-layout.ts +1 -1
  150. package/src/migrate/session-defaults.ts +138 -0
  151. package/src/migrate/skill-names.ts +68 -21
  152. package/src/pr/frames.ts +14 -3
  153. package/src/pr/local.ts +4 -69
  154. package/src/records/validate.ts +31 -0
  155. package/src/sync/check.ts +15 -0
  156. package/src/tasks/board.ts +854 -0
  157. package/src/tasks/record.ts +20 -1
  158. package/src/tasks/validate.ts +206 -9
  159. package/src/tooling/stamp.ts +56 -12
  160. package/src/worktrees/stop.ts +92 -0
  161. package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
  162. package/tooling/astro/reference.md +2 -2
  163. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
  164. package/tooling/base/configs/commitlint.config.js +0 -20
  165. package/tooling/base/reference.md +1 -1
  166. package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
  167. package/tooling/claude/reference.md +13 -7
  168. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
  169. package/tooling/claude/seeds/.claude/settings.json +8 -0
  170. package/tooling/claude/user/settings.template.json +0 -4
  171. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
  172. package/tooling/public/configs/.github/dependabot.yml +12 -0
  173. package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
  174. package/tooling/public/manifest.toml +8 -0
  175. package/tooling/public/reference.md +53 -0
  176. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
  177. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
  178. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
  179. package/tooling/public/seeds/.release-please-manifest.json +3 -0
  180. package/tooling/public/seeds/CONTRIBUTING.md +50 -0
  181. package/tooling/public/seeds/LICENSE +21 -0
  182. package/tooling/public/seeds/SECURITY.md +13 -0
  183. package/tooling/public/seeds/release-please-config.json +12 -0
  184. package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
  185. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
  186. package/tooling/web/configs/.github/workflows/verify.yml +15 -12
  187. /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
  188. /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
  189. /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
  190. /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
  191. /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
  192. /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
  193. /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
  194. /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
  195. /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
@@ -3,6 +3,17 @@ import type { Command } from 'commander'
3
3
  import { execa } from 'execa'
4
4
  import { gitEnv } from '@/git/env'
5
5
  import { type AnswersOutcome, planAnswers } from '@/tasks/answers'
6
+ import {
7
+ type BoardOutcome,
8
+ type BoardRefused,
9
+ blockTask,
10
+ moveTask,
11
+ PLANNED_WAITING,
12
+ type Position,
13
+ placeTask,
14
+ promoteTask,
15
+ WAITING_LIMIT,
16
+ } from '@/tasks/board'
6
17
  import { type BranchOutcome, planBranch } from '@/tasks/branch'
7
18
  import {
8
19
  type ArchiveOutcome,
@@ -40,6 +51,7 @@ import {
40
51
  recordPullRequest,
41
52
  } from '@/tasks/record'
42
53
  import {
54
+ type BoardGroup,
43
55
  type Finding,
44
56
  type FolderClaim,
45
57
  type SharedFiles,
@@ -118,6 +130,7 @@ interface PullRequestCommandOptions {
118
130
 
119
131
  interface PlanLinkCommandOptions {
120
132
  readonly add?: boolean
133
+ readonly waiting?: string
121
134
  readonly json?: boolean
122
135
  readonly root?: string
123
136
  }
@@ -140,6 +153,31 @@ interface NextLabelCommandOptions {
140
153
  readonly root?: string
141
154
  }
142
155
 
156
+ interface BoardCommandOptions {
157
+ readonly json?: boolean
158
+ readonly root?: string
159
+ }
160
+
161
+ interface PositionCommandOptions {
162
+ readonly top?: boolean
163
+ readonly bottom?: boolean
164
+ readonly before?: string
165
+ readonly after?: string
166
+ }
167
+
168
+ interface PlaceCommandOptions
169
+ extends BoardCommandOptions, PositionCommandOptions {
170
+ readonly group: string
171
+ readonly waiting?: string
172
+ }
173
+
174
+ interface PromoteCommandOptions extends BoardCommandOptions {
175
+ readonly waiting?: string
176
+ }
177
+
178
+ interface MoveCommandOptions
179
+ extends BoardCommandOptions, PositionCommandOptions {}
180
+
143
181
  export function register(program: Command): void {
144
182
  const tasks = program
145
183
  .command('tasks')
@@ -259,6 +297,10 @@ export function register(program: Command): void {
259
297
  ' every task file carries a board row or a backlog line, never both',
260
298
  ' no task carries more than one row',
261
299
  ' every Run now row names the files it touches',
300
+ ' every Touches cell matches what its plans derive',
301
+ ' every group header is the set of columns the group owns',
302
+ ` no Waiting on cell renders past ${WAITING_LIMIT} characters`,
303
+ ' a blocker citing a bare vNNN.N label settles like a linked one',
262
304
  ' a file two Run now rows share is listed, never failed',
263
305
  '',
264
306
  'Refusals, read off `reason`:',
@@ -493,6 +535,10 @@ export function register(program: Command): void {
493
535
  '--add',
494
536
  'Append the plan to the line, for a task sliced over plans',
495
537
  )
538
+ .option(
539
+ '--waiting <text>',
540
+ `The Waiting on cell a promoted row carries, "${PLANNED_WAITING}" when omitted`,
541
+ )
496
542
  .option('--json', 'Emit a machine-readable record on stdout')
497
543
  .option('--root <path>', 'Board root, defaulting to the main worktree')
498
544
  .addHelpText(
@@ -521,6 +567,10 @@ export function register(program: Command): void {
521
567
  'sliced task links one plan per slice. It reports unchanged when the',
522
568
  'plan is already listed and is allowed on a line holding several.',
523
569
  '',
570
+ 'A row under Needs a plan moves to the bottom of Up next through the',
571
+ 'board verbs, and `board` in the record says whether it moved and why',
572
+ 'not. A row elsewhere, or a task with no row, stays where it is.',
573
+ '',
524
574
  'Examples:',
525
575
  ' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
526
576
  ' canon tasks plan-link v28.1-trigger-escalation second-slice --add',
@@ -661,6 +711,327 @@ export function register(program: Command): void {
661
711
  .action(async (opts: ListCommandOptions) => {
662
712
  process.exitCode = await runList(opts)
663
713
  })
714
+
715
+ registerBoardWrites(tasks)
716
+ }
717
+
718
+ const BOARD_WRITE_REFUSALS = [
719
+ 'Refusals, read off `reason`:',
720
+ ' no-board the repository holds no `.canon/tasks/` board',
721
+ ' no-ordering the board has no priority.md',
722
+ ' no-match no task file carries the stem',
723
+ ' no-row the task has no row on the board',
724
+ ' unreadable-row a row names no task, or its header cannot place its blocker',
725
+ ' board-busy another board verb held the lock past the wait, so run it again',
726
+ ' board-changed a hand edit landed while this one rendered, so run it again',
727
+ ' group-rule the target group forbids the row, naming the validate kind',
728
+ ' no-column the row sits under Run now, which has no Waiting on cell',
729
+ ' top-group the row already sits under Run now',
730
+ ` cell-long the Waiting on cell renders past ${WAITING_LIMIT} characters`,
731
+ ' bad-input a malformed group, position, or Waiting on text',
732
+ '',
733
+ 'Exit codes:',
734
+ ' 0 the board was written',
735
+ ' 1 refused, with the reason on stderr or in the JSON record',
736
+ '',
737
+ 'Every write reads priority.md once, renders every table from the task',
738
+ 'files and their plans, and writes it once. Task, Touches, and Plan are',
739
+ 'derived, so only Waiting on is typed. Safe from a linked worktree, since',
740
+ 'it resolves the board root in-process.',
741
+ ]
742
+
743
+ const GROUP_FLAGS: Readonly<Record<string, BoardGroup>> = {
744
+ 'run-now': 'Run now',
745
+ 'up-next': 'Up next',
746
+ 'needs-a-plan': 'Needs a plan',
747
+ }
748
+
749
+ function withPosition(command: Command): Command {
750
+ return command
751
+ .option('--top', 'Put the row first in its group')
752
+ .option('--bottom', 'Put the row last in its group, the default')
753
+ .option('--before <task>', 'Put the row right above this sibling')
754
+ .option('--after <task>', 'Put the row right below this sibling')
755
+ }
756
+
757
+ function registerBoardWrites(tasks: Command): void {
758
+ withPosition(
759
+ tasks
760
+ .command('place')
761
+ .description(
762
+ 'Put a task under a board group, from another group, the backlog, or nowhere',
763
+ )
764
+ .argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
765
+ .requiredOption(
766
+ '--group <group>',
767
+ 'Target group: run-now, up-next, or needs-a-plan',
768
+ )
769
+ .option('--waiting <text>', 'The Waiting on cell, kept when omitted'),
770
+ )
771
+ .helpOption('-h, --help', 'Show this help message')
772
+ .option('--json', 'Emit a machine-readable record on stdout')
773
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
774
+ .addHelpText(
775
+ 'after',
776
+ [
777
+ '',
778
+ ...BOARD_WRITE_REFUSALS,
779
+ '',
780
+ 'A backlog line for the task is removed in the same call. The row keeps',
781
+ 'its Waiting on cell when --waiting is omitted, and a row new to Up next',
782
+ 'or Needs a plan has none to keep, so it needs one.',
783
+ '',
784
+ 'Examples:',
785
+ ' canon tasks place v28.1-trigger-escalation --group needs-a-plan --bottom --waiting "Not planned yet"',
786
+ ' canon tasks place v28.1-trigger-escalation --group up-next --before v28.2-other --json',
787
+ '',
788
+ ].join('\n'),
789
+ )
790
+ .action(async (task: string, opts: PlaceCommandOptions) => {
791
+ process.exitCode = await runPlace(task, opts)
792
+ })
793
+
794
+ tasks
795
+ .command('promote')
796
+ .description('Move a row one group closer to Run now, to its bottom')
797
+ .argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
798
+ .helpOption('-h, --help', 'Show this help message')
799
+ .option(
800
+ '--waiting <text>',
801
+ `The Waiting on cell a row entering Up next carries, "${PLANNED_WAITING}" when omitted`,
802
+ )
803
+ .option('--json', 'Emit a machine-readable record on stdout')
804
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
805
+ .addHelpText(
806
+ 'after',
807
+ [
808
+ '',
809
+ ...BOARD_WRITE_REFUSALS,
810
+ '',
811
+ 'Up next moves to Run now, which reads as dispatched, so a dispatcher',
812
+ 'runs it once a launch succeeds. Needs a plan moves to Up next.',
813
+ '',
814
+ 'Examples:',
815
+ ' canon tasks promote v28.1-trigger-escalation',
816
+ ' canon tasks promote v28.1-trigger-escalation --json',
817
+ '',
818
+ ].join('\n'),
819
+ )
820
+ .action(async (task: string, opts: PromoteCommandOptions) => {
821
+ process.exitCode = await runPromote(task, opts)
822
+ })
823
+
824
+ withPosition(
825
+ tasks
826
+ .command('move')
827
+ .description('Reposition a row inside the group it sits in')
828
+ .argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation'),
829
+ )
830
+ .helpOption('-h, --help', 'Show this help message')
831
+ .option('--json', 'Emit a machine-readable record on stdout')
832
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
833
+ .addHelpText(
834
+ 'after',
835
+ [
836
+ '',
837
+ ...BOARD_WRITE_REFUSALS,
838
+ '',
839
+ 'Examples:',
840
+ ' canon tasks move v28.1-trigger-escalation --top',
841
+ ' canon tasks move v28.1-trigger-escalation --after v28.2-other --json',
842
+ '',
843
+ ].join('\n'),
844
+ )
845
+ .action(async (task: string, opts: MoveCommandOptions) => {
846
+ process.exitCode = await runMove(task, opts)
847
+ })
848
+
849
+ tasks
850
+ .command('block')
851
+ .description("Rewrite a row's Waiting on cell")
852
+ .argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
853
+ .argument(
854
+ '<waiting>',
855
+ `What the row waits on, at most ${WAITING_LIMIT} rendered characters`,
856
+ )
857
+ .helpOption('-h, --help', 'Show this help message')
858
+ .option('--json', 'Emit a machine-readable record on stdout')
859
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
860
+ .addHelpText(
861
+ 'after',
862
+ [
863
+ '',
864
+ ...BOARD_WRITE_REFUSALS,
865
+ '',
866
+ 'A link counts as its label alone toward the cap, since only the label',
867
+ 'renders.',
868
+ '',
869
+ 'Examples:',
870
+ ' canon tasks block v28.1-trigger-escalation "[v28.2](v28.2-other.md) merging"',
871
+ ' canon tasks block v28.1-trigger-escalation "The operator\'s call on the scope" --json',
872
+ '',
873
+ ].join('\n'),
874
+ )
875
+ .action(
876
+ async (task: string, waiting: string, opts: BoardCommandOptions) => {
877
+ process.exitCode = await runBlock(task, waiting, opts)
878
+ },
879
+ )
880
+ }
881
+
882
+ /**
883
+ * Reads the one position flag a call may carry. Two flags at once is refused
884
+ * rather than resolved by precedence, since either reading moves a row the
885
+ * caller did not mean to.
886
+ */
887
+ function positionFrom(
888
+ opts: PositionCommandOptions,
889
+ ): Position | undefined | BoardRefused {
890
+ const given: Position[] = [
891
+ ...(opts.top ? [{ at: 'top' } as const] : []),
892
+ ...(opts.bottom ? [{ at: 'bottom' } as const] : []),
893
+ ...(opts.before ? [{ before: opts.before }] : []),
894
+ ...(opts.after ? [{ after: opts.after }] : []),
895
+ ]
896
+
897
+ if (given.length > 1) {
898
+ return {
899
+ ok: false,
900
+ reason: 'bad-input',
901
+ message: 'Pass one of --top, --bottom, --before, or --after.',
902
+ detail: [],
903
+ }
904
+ }
905
+
906
+ return given[0]
907
+ }
908
+
909
+ async function runPlace(
910
+ task: string,
911
+ opts: PlaceCommandOptions,
912
+ ): Promise<number> {
913
+ const emitJson = opts.json ?? false
914
+ const group = GROUP_FLAGS[opts.group]
915
+ if (!group) {
916
+ return reportRecord(
917
+ 'canon tasks place',
918
+ {
919
+ ok: false,
920
+ reason: 'bad-input',
921
+ message: `Not a group: ${opts.group}. Pass run-now, up-next, or needs-a-plan.`,
922
+ detail: [opts.group],
923
+ },
924
+ emitJson,
925
+ process.cwd(),
926
+ )
927
+ }
928
+
929
+ const position = positionFrom(opts)
930
+ if (position && 'ok' in position) {
931
+ return reportRecord('canon tasks place', position, emitJson, process.cwd())
932
+ }
933
+
934
+ const root = opts.root ?? (await mainWorktreeRoot())
935
+ const outcome = await placeTask(root, task, group, {
936
+ waiting: opts.waiting,
937
+ position,
938
+ })
939
+
940
+ return reportBoardWrite('canon tasks place', outcome, emitJson, root)
941
+ }
942
+
943
+ async function runPromote(
944
+ task: string,
945
+ opts: PromoteCommandOptions,
946
+ ): Promise<number> {
947
+ const root = opts.root ?? (await mainWorktreeRoot())
948
+ const outcome = await promoteTask(root, task, { waiting: opts.waiting })
949
+
950
+ return reportBoardWrite(
951
+ 'canon tasks promote',
952
+ outcome,
953
+ opts.json ?? false,
954
+ root,
955
+ )
956
+ }
957
+
958
+ async function runMove(
959
+ task: string,
960
+ opts: MoveCommandOptions,
961
+ ): Promise<number> {
962
+ const emitJson = opts.json ?? false
963
+ const position = positionFrom(opts)
964
+ if (position === undefined || 'ok' in position) {
965
+ return reportRecord(
966
+ 'canon tasks move',
967
+ position ?? {
968
+ ok: false,
969
+ reason: 'bad-input',
970
+ message:
971
+ 'No position named. Pass --top, --bottom, --before, or --after.',
972
+ detail: [],
973
+ },
974
+ emitJson,
975
+ process.cwd(),
976
+ )
977
+ }
978
+
979
+ const root = opts.root ?? (await mainWorktreeRoot())
980
+ const outcome = await moveTask(root, task, position)
981
+
982
+ return reportBoardWrite('canon tasks move', outcome, emitJson, root)
983
+ }
984
+
985
+ async function runBlock(
986
+ task: string,
987
+ waiting: string,
988
+ opts: BoardCommandOptions,
989
+ ): Promise<number> {
990
+ const root = opts.root ?? (await mainWorktreeRoot())
991
+ const outcome = await blockTask(root, task, waiting)
992
+
993
+ return reportBoardWrite(
994
+ 'canon tasks block',
995
+ outcome,
996
+ opts.json ?? false,
997
+ root,
998
+ )
999
+ }
1000
+
1001
+ function reportBoardWrite(
1002
+ title: string,
1003
+ outcome: BoardOutcome,
1004
+ emitJson: boolean,
1005
+ root: string,
1006
+ ): number {
1007
+ if (!outcome.ok) return reportRecord(title, outcome, emitJson, root)
1008
+
1009
+ if (emitJson) {
1010
+ process.stdout.write(
1011
+ `${JSON.stringify({
1012
+ ok: true,
1013
+ root,
1014
+ task: outcome.stem,
1015
+ path: relative(root, outcome.path),
1016
+ group: outcome.group,
1017
+ from: outcome.from ?? null,
1018
+ position: outcome.position,
1019
+ backlog: outcome.backlog,
1020
+ })}\n`,
1021
+ )
1022
+ return 0
1023
+ }
1024
+
1025
+ intro(title)
1026
+ logStep('Written')
1027
+ logInfo(
1028
+ `${outcome.stem} sits at ${outcome.position} under ${outcome.group}${outcome.from && outcome.from !== outcome.group ? `, from ${outcome.from}` : ''}`,
1029
+ )
1030
+ if (outcome.backlog) logRemove('its backlog line')
1031
+ logAdd(relative(root, outcome.path))
1032
+ outro()
1033
+
1034
+ return 0
664
1035
  }
665
1036
 
666
1037
  function collectPosition(value: string, previous: string[]): string[] {
@@ -832,7 +1203,13 @@ async function runPlanLink(
832
1203
  ): Promise<number> {
833
1204
  const emitJson = opts.json ?? false
834
1205
  const root = opts.root ?? (await mainWorktreeRoot())
835
- const outcome = await recordPlan(root, task, plan, opts.add ?? false)
1206
+ const outcome = await recordPlan(
1207
+ root,
1208
+ task,
1209
+ plan,
1210
+ opts.add ?? false,
1211
+ opts.waiting,
1212
+ )
836
1213
 
837
1214
  return reportPlanLink(outcome, emitJson, root)
838
1215
  }
@@ -900,7 +1277,7 @@ async function runOutcome(
900
1277
 
901
1278
  function reportRecord(
902
1279
  title: string,
903
- refused: RecordRefused,
1280
+ refused: RecordRefused | BoardRefused,
904
1281
  emitJson: boolean,
905
1282
  root: string,
906
1283
  ): number {
@@ -985,6 +1362,7 @@ function reportPlanLink(
985
1362
  plan: outcome.plan,
986
1363
  action: outcome.action,
987
1364
  replaced: outcome.replaced,
1365
+ board: { ...outcome.board, from: outcome.board.from ?? null },
988
1366
  })}\n`,
989
1367
  )
990
1368
  return 0
@@ -995,6 +1373,9 @@ function reportPlanLink(
995
1373
  logInfo(`${outcome.stem} names plan ${outcome.plan}`)
996
1374
  if (outcome.replaced) logInfo(`replaced ${outcome.replaced}`)
997
1375
  if (outcome.action !== 'unchanged') logAdd(relative(root, outcome.path))
1376
+ logInfo(
1377
+ `board row ${outcome.board.moved ? '' : 'not moved, '}${outcome.board.note}`,
1378
+ )
998
1379
  outro()
999
1380
 
1000
1381
  return 0
@@ -18,7 +18,7 @@ import {
18
18
  } from '@/tooling/manifest'
19
19
  import { readReference, resolveReference } from '@/tooling/read'
20
20
  import { scan, type ScanResult } from '@/tooling/scan'
21
- import { recordedStacks, recordToolingChain } from '@/tooling/stamp'
21
+ import { recordToolingChain } from '@/tooling/stamp'
22
22
  import { subfolderPath } from '@/tooling/subfolder'
23
23
  import { verifyStack } from '@/tooling/verify-stack'
24
24
  import {
@@ -513,8 +513,8 @@ async function stampChain(chain: Manifest[], target: string): Promise<void> {
513
513
  new Date(),
514
514
  )
515
515
 
516
- if (recorded) {
517
- logInfo(`Recorded chain: ${recordedStacks(chain).join(' < ')}`)
516
+ if (recorded !== undefined) {
517
+ logInfo(`Recorded chain: ${recorded.join(' < ')}`)
518
518
  return
519
519
  }
520
520
 
@@ -17,7 +17,9 @@ import {
17
17
  type WorktreeVerdict,
18
18
  } from '@/worktrees/reclaim'
19
19
  import { mainWorktreeRoot } from '@/git/worktree'
20
+ import { machineListenerReader } from '@/listeners/machine'
20
21
  import { repairBareFlag } from '@/worktrees/bare-flag'
22
+ import { type StopReport, stopListeners } from '@/worktrees/stop'
21
23
  import {
22
24
  removeReclaimable,
23
25
  type RemovalOutcome,
@@ -57,6 +59,12 @@ export interface ReclaimRecord {
57
59
  readonly removed: number
58
60
  readonly failed: number
59
61
  readonly pruned: boolean
62
+ /**
63
+ * Processes signaled, or that a dry run would signal, for listening inside a
64
+ * reclaimed worktree. Null when the machine offered no way to read
65
+ * listeners, which says nothing about what is still running.
66
+ */
67
+ readonly stopped: number | null
60
68
  readonly outcomes: readonly RemovalOutcome[]
61
69
  }
62
70
 
@@ -79,6 +87,8 @@ export type ReclaimSource =
79
87
  * from a run that was asked not to act.
80
88
  */
81
89
  readonly removal: RemovalReport | null
90
+ /** Null when no stop ran, which is a reading with nothing reclaimable. */
91
+ readonly stop?: StopReport | null
82
92
  }
83
93
 
84
94
  const REFUSALS: Record<Refusal, string> = {
@@ -198,6 +208,14 @@ export function register(program: Command): void {
198
208
  'deletes its branch, and a run carrying a directory that is already',
199
209
  'gone sweeps stale registrations once before the branch deletes.',
200
210
  '',
211
+ 'Ahead of each removal it sends SIGTERM to every process listening from',
212
+ 'inside a reclaimable worktree, so no dev server outlives the tree it',
213
+ 'serves. A listener in the main checkout, in a refused worktree, or in',
214
+ 'a worktree nested under another is left alone. A dry run signals',
215
+ 'nothing and still reports what it would stop. "stopped" in the record',
216
+ 'counts those processes, and reads null on a machine with neither lsof',
217
+ 'nor /proc, where no listener could be read.',
218
+ '',
201
219
  'It deletes only what a reading called reclaimable. A worktree a live',
202
220
  'session holds is refused rather than removed here. `claude rm <id>`',
203
221
  'is meant to remove the session and its worktree together, but its',
@@ -324,11 +342,13 @@ export function reclaimRecord(source: ReclaimSource): ReclaimRecord {
324
342
  removed: 0,
325
343
  failed: 0,
326
344
  pruned: false,
345
+ stopped: 0,
327
346
  outcomes: [],
328
347
  }
329
348
  }
330
349
 
331
350
  const outcomes = source.removal?.outcomes ?? []
351
+ const stop = source.stop ?? null
332
352
 
333
353
  return {
334
354
  reason: null,
@@ -338,10 +358,30 @@ export function reclaimRecord(source: ReclaimSource): ReclaimRecord {
338
358
  removed: outcomes.filter((outcome) => outcome.removed).length,
339
359
  failed: outcomes.filter((outcome) => !outcome.removed).length,
340
360
  pruned: source.removal?.pruned ?? false,
361
+ stopped: stop === null ? 0 : countStopped(stop),
341
362
  outcomes,
342
363
  }
343
364
  }
344
365
 
366
+ function countStopped(stop: StopReport): number | null {
367
+ if (stop.kind === 'unread') return null
368
+ return stop.stopped.reduce((sum, server) => sum + server.pids.length, 0)
369
+ }
370
+
371
+ /** Names each server a stop took, or would take on a dry run, and says when none could be read. */
372
+ function reportStop(stop: StopReport, dryRun: boolean): void {
373
+ if (stop.kind === 'unread') {
374
+ logWarn(
375
+ 'This machine offers neither lsof nor /proc, so listening servers were not read and none was stopped.',
376
+ )
377
+ return
378
+ }
379
+ const verb = dryRun ? 'Would stop' : 'Stopped'
380
+ for (const { path, ports } of stop.stopped) {
381
+ logInfo(`${verb} the server on ${ports.join(', ')} in ${path}`)
382
+ }
383
+ }
384
+
345
385
  function emitReclaim(opts: ReclaimCommandOptions, source: ReclaimSource): void {
346
386
  if (opts.json !== true) return
347
387
  process.stdout.write(`${JSON.stringify(reclaimRecord(source))}\n`)
@@ -381,8 +421,14 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
381
421
  logInfo(plural(reclaimable.length, 'worktree'))
382
422
  pipeOutput(reclaimable.map(describe).join('\n'))
383
423
 
424
+ const stop = await stopListeners(
425
+ reclaimable.map((entry) => entry.path),
426
+ { reader: machineListenerReader(), dryRun },
427
+ )
428
+
384
429
  if (dryRun) {
385
430
  logStep('Dry run')
431
+ reportStop(stop, dryRun)
386
432
  logWarn('Nothing was removed. Run this again without --dry-run to remove.')
387
433
  outro()
388
434
  emitReclaim(opts, {
@@ -390,6 +436,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
390
436
  dryRun,
391
437
  reclaimable: reclaimable.length,
392
438
  removal: null,
439
+ stop,
393
440
  })
394
441
  return 0
395
442
  }
@@ -403,6 +450,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
403
450
  })
404
451
 
405
452
  logStep('Removed')
453
+ reportStop(stop, dryRun)
406
454
  for (const outcome of removal.outcomes) reportOutcome(outcome)
407
455
  if (removal.pruned) {
408
456
  logInfo(
@@ -423,6 +471,7 @@ async function runReclaim(opts: ReclaimCommandOptions): Promise<number> {
423
471
  dryRun,
424
472
  reclaimable: reclaimable.length,
425
473
  removal,
474
+ stop,
426
475
  })
427
476
  return failed.length > 0 ? 1 : 0
428
477
  }
@@ -209,7 +209,7 @@ const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
209
209
  * reading that field for a gain of one word.
210
210
  *
211
211
  * `claude/skills/context-fold/references/context.md` opens its scope by handing figures to the
212
- * `draft-figure` skill, so a marker reported in a diagrams entry would cite a
212
+ * `draw-figure` skill, so a marker reported in a diagrams entry would cite a
213
213
  * rule that entry's own standard routes elsewhere. The length and table
214
214
  * checkpoints are quoted from the same standard and keep reaching every audited
215
215
  * folder, because a threshold on how far a reader travels generalizes across
package/src/demo/beats.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Reads the human-facing draft `draft-screencast` writes. Nothing here knows
2
+ * Reads the human-facing draft `script-screencast` writes. Nothing here knows
3
3
  * about a browser: the draft is prose aimed at a person, and turning it into
4
4
  * something executable is `@/demo/compile`'s job.
5
5
  */
@@ -380,6 +380,21 @@ export const STAGES: readonly Stage[] = [
380
380
  ],
381
381
  success: 'Rule citations resolve',
382
382
  },
383
+ {
384
+ // Reads the whole rule corpus and fails only on a folder the verb lists as
385
+ // gated, so the finding count prints on every push while the rewrite moves
386
+ // folder by folder toward zero.
387
+ id: 'rule-shape',
388
+ label: 'Rule shape',
389
+ checks: [
390
+ {
391
+ kind: 'cli',
392
+ argv: ['gov', 'audit', '--gate'],
393
+ failure:
394
+ 'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit.',
395
+ },
396
+ ],
397
+ },
383
398
  {
384
399
  // A citation naming the old record root resolves to nothing once the
385
400
  // folders have moved, and the sweep that would repoint it only runs when a