@erclx/canon 5.46.0 → 5.48.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 (55) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/git-followup/SKILL.md +1 -0
  3. package/claude/skills/git-pr/REQUIREMENT.md +2 -2
  4. package/claude/skills/git-pr/SKILL.md +2 -1
  5. package/claude/skills/git-pr/references/closing-issues.md +2 -0
  6. package/claude/skills/git-pr/references/pr.md +58 -7
  7. package/claude/skills/review-address/SKILL.md +18 -6
  8. package/claude/skills/review-address/references/post-review-findings.md +4 -1
  9. package/claude/skills/review-address/references/rebase-only.md +14 -2
  10. package/claude/skills/review-pr/SKILL.md +9 -2
  11. package/claude/skills/review-pr/references/body-variants.md +29 -8
  12. package/claude/skills/review-pr/references/post.md +29 -10
  13. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  14. package/claude/skills/review-ui/SKILL.md +4 -3
  15. package/claude/skills/review-ui/references/post.md +16 -2
  16. package/claude/skills/review-ui/references/tick.md +7 -1
  17. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +4 -2
  19. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  20. package/claude/skills/role-reviewer/SKILL.md +4 -1
  21. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  22. package/claude/skills/role-worker/SKILL.md +1 -0
  23. package/claude/skills/task-board/references/decline.md +2 -2
  24. package/claude/skills/task-board/references/tasks.md +4 -2
  25. package/claude/skills/task-complete/SKILL.md +1 -0
  26. package/claude/skills/task-complete/references/archive.md +5 -3
  27. package/claude/skills/ui-checklist/SKILL.md +3 -3
  28. package/docs/agents/migrate.md +2 -2
  29. package/docs/target/migrations.md +4 -2
  30. package/docs/workflow/operating-model.md +2 -2
  31. package/docs/workflow/zshrc-aliases.md +3 -0
  32. package/package.json +1 -1
  33. package/scripts/core/bootstrap.sh +1 -0
  34. package/src/commands/claude.ts +4 -0
  35. package/src/commands/labels.ts +66 -1
  36. package/src/commands/migrate.ts +5 -4
  37. package/src/commands/pr.ts +405 -30
  38. package/src/commands/sessions.ts +25 -11
  39. package/src/commands/tasks.ts +11 -1
  40. package/src/labels/density.ts +363 -0
  41. package/src/migrate/session-defaults.ts +20 -5
  42. package/src/pr/evidence.ts +1 -1
  43. package/src/pr/lift.ts +19 -0
  44. package/src/pr/merge.ts +175 -0
  45. package/src/pr/rest.ts +3 -0
  46. package/src/pr/review-scope.ts +40 -1
  47. package/src/pr/tick.ts +109 -0
  48. package/src/sessions/live.ts +26 -0
  49. package/src/sessions/resolve.ts +62 -11
  50. package/src/tasks/archive.ts +32 -8
  51. package/src/tasks/closes.ts +9 -5
  52. package/src/tasks/validate.ts +4 -2
  53. package/tooling/base/seeds/.github/pull_request_template.md +1 -1
  54. package/tooling/claude/reference.md +3 -1
  55. package/tooling/claude/seeds/.claude/settings.json +1 -1
@@ -107,6 +107,7 @@ export interface ArchiveSuccess {
107
107
  readonly from: string
108
108
  readonly to: string
109
109
  readonly priorityRowRemoved: boolean
110
+ readonly backlogRowRemoved: boolean
110
111
  readonly indexRegenerated: boolean
111
112
  /** Every live plan the task cited that no other live task still holds, in line order. */
112
113
  readonly plans: readonly PlanMove[]
@@ -114,6 +115,8 @@ export interface ArchiveSuccess {
114
115
  readonly ready: ReadyMove | undefined
115
116
  readonly closed: number
116
117
  readonly cut: number
118
+ /** Unticked `Operator check:` outcomes the archive left as they were. */
119
+ readonly operatorChecks: readonly string[]
117
120
  /** Every task file whose links to the moved task were rewritten. */
118
121
  readonly relinked: readonly InboundRelink[]
119
122
  /** Every task file the pass could not read or write, left as it was. */
@@ -158,6 +161,8 @@ export interface TaskOutcomes {
158
161
  readonly open: readonly string[]
159
162
  readonly closed: readonly string[]
160
163
  readonly cut: readonly string[]
164
+ /** Unticked outcomes only the operator can check, which hold nothing open. */
165
+ readonly operator: readonly string[]
161
166
  }
162
167
 
163
168
  export function tasksDir(root: string): string {
@@ -196,6 +201,9 @@ const BULLET_PATTERN = /^- (?:\[([ xX])\] ?)?(.*)$/
196
201
  */
197
202
  const STRUCK_BODY_PATTERN = /^~~.+~~/
198
203
 
204
+ /** The one spelling of an operator-owned outcome, anchored to the body's start. */
205
+ const OPERATOR_BODY_PATTERN = /^Operator check: ?\S/
206
+
199
207
  /**
200
208
  * Marks the lines sitting inside a fenced block, the fence delimiters included.
201
209
  * A checkbox in a sample a task displays is not an outcome the task claims, and
@@ -231,6 +239,7 @@ export function readOutcomes(text: string): TaskOutcomes {
231
239
  const open: string[] = []
232
240
  const closed: string[] = []
233
241
  const cut: string[] = []
242
+ const operator: string[] = []
234
243
  const lines = text.split('\n')
235
244
  const fenced = fenceMask(lines)
236
245
 
@@ -251,13 +260,17 @@ export function readOutcomes(text: string): TaskOutcomes {
251
260
  if (box === undefined) continue
252
261
 
253
262
  if (box === ' ') {
254
- open.push(body)
263
+ if (OPERATOR_BODY_PATTERN.test(body)) {
264
+ operator.push(body)
265
+ } else {
266
+ open.push(body)
267
+ }
255
268
  } else {
256
269
  closed.push(body)
257
270
  }
258
271
  }
259
272
 
260
- return { open, closed, cut }
273
+ return { open, closed, cut, operator }
261
274
  }
262
275
 
263
276
  /**
@@ -556,7 +569,7 @@ function isRowFor(line: string, target: string): boolean {
556
569
  }
557
570
 
558
571
  /**
559
- * Drops the declined task's bullet from the backlog. A backlog line is a
572
+ * Drops the task's bullet from the backlog. A backlog line is a
560
573
  * bullet carrying a link rather than a table row, so the match is a bullet
561
574
  * prefix and the link target rather than `isRowFor`'s pipe-delimited cell.
562
575
  */
@@ -571,9 +584,17 @@ export function removeBacklogRow(
571
584
  return { text: kept.join('\n'), removed: kept.length !== lines.length }
572
585
  }
573
586
 
587
+ /**
588
+ * A bullet is about a task when its first link targets it. A later link is a
589
+ * reference, such as a note on what the bullet waits for, and matching anywhere
590
+ * would drop that other task's line from a file no history backs.
591
+ */
574
592
  function isBulletFor(line: string, target: string): boolean {
575
593
  const trimmed = line.trimStart()
576
- return /^[-*]\s/.test(trimmed) && trimmed.includes(target)
594
+ if (!/^[-*]\s/.test(trimmed)) return false
595
+
596
+ const first = trimmed.indexOf('](')
597
+ return first !== -1 && trimmed.startsWith(target, first)
577
598
  }
578
599
 
579
600
  /**
@@ -896,8 +917,8 @@ async function resolveStem(
896
917
  }
897
918
 
898
919
  /**
899
- * Archives one task as a single unit: the move, the ordering-row removal, and
900
- * the index regen. The hook and `task-complete` both call this, so every gate
920
+ * Archives one task as a single unit: the move, the ordering-row and backlog
921
+ * bullet removal, and the index regen. The hook and `task-complete` both call this, so every gate
901
922
  * refuses rather than reports. A caller with nobody watching cannot act on a
902
923
  * warning, and two callers gating differently is the drift this exists to stop.
903
924
  */
@@ -942,7 +963,7 @@ export async function archiveTask(
942
963
  )
943
964
  }
944
965
 
945
- const { open, closed, cut } = readOutcomes(text)
966
+ const { open, closed, cut, operator } = readOutcomes(text)
946
967
 
947
968
  if (open.length > 0) {
948
969
  return refuse(
@@ -952,7 +973,7 @@ export async function archiveTask(
952
973
  )
953
974
  }
954
975
 
955
- if (closed.length === 0 && cut.length === 0) {
976
+ if (closed.length === 0 && cut.length === 0 && operator.length === 0) {
956
977
  return refuse(
957
978
  'no-outcomes',
958
979
  `${stem} carries no outcomes, so nothing marks it shipped.`,
@@ -996,6 +1017,7 @@ export async function archiveTask(
996
1017
 
997
1018
  const { relinked, failed } = await repointInboundLinks(root, from, to)
998
1019
  const priorityRowRemoved = await clearPriorityRow(dir, stem)
1020
+ const backlogRowRemoved = await clearBacklogRow(dir, stem)
999
1021
  const regen = await regenOne(dir, { dryRun: false })
1000
1022
 
1001
1023
  return {
@@ -1004,11 +1026,13 @@ export async function archiveTask(
1004
1026
  from,
1005
1027
  to,
1006
1028
  priorityRowRemoved,
1029
+ backlogRowRemoved,
1007
1030
  indexRegenerated: regen.action === 'written',
1008
1031
  plans: plans.moves,
1009
1032
  ready: ready || undefined,
1010
1033
  closed: closed.length,
1011
1034
  cut: cut.length,
1035
+ operatorChecks: operator,
1012
1036
  relinked,
1013
1037
  relinkFailed: failed,
1014
1038
  }
@@ -29,6 +29,8 @@ export interface Closure {
29
29
  readonly issues: readonly number[]
30
30
  readonly closing: boolean
31
31
  readonly reason: ClosingReason
32
+ /** Unticked operator checks, read once the outcome test runs. */
33
+ readonly operatorChecks?: readonly string[]
32
34
  }
33
35
 
34
36
  export interface TaskClosure extends Closure {
@@ -108,16 +110,18 @@ export function decideClosing(
108
110
  return { issues, closing: false, reason: 'pending-branch' }
109
111
  }
110
112
 
111
- const { open, closed, cut } = readOutcomes(text)
113
+ const { open, closed, cut, operator } = readOutcomes(text)
114
+ const operatorChecks = operator
115
+
112
116
  if (open.length > 0) {
113
- return { issues, closing: false, reason: 'open-outcomes' }
117
+ return { issues, closing: false, reason: 'open-outcomes', operatorChecks }
114
118
  }
115
119
 
116
- if (closed.length === 0 && cut.length === 0) {
117
- return { issues, closing: false, reason: 'no-outcomes' }
120
+ if (closed.length === 0 && cut.length === 0 && operator.length === 0) {
121
+ return { issues, closing: false, reason: 'no-outcomes', operatorChecks }
118
122
  }
119
123
 
120
- return { issues, closing: true, reason: 'last-slice' }
124
+ return { issues, closing: true, reason: 'last-slice', operatorChecks }
121
125
  }
122
126
 
123
127
  /** Resolves the task through the same selector `pull-request` takes. */
@@ -1315,8 +1315,10 @@ async function checkCitedTask(
1315
1315
  }
1316
1316
 
1317
1317
  const text = await readFile(live, 'utf8')
1318
- const { open, closed } = readOutcomes(text)
1319
- if (open.length > 0 || closed.length === 0) return nothing()
1318
+ const { open, closed, operator } = readOutcomes(text)
1319
+ if (open.length > 0 || closed.length + operator.length === 0) {
1320
+ return nothing()
1321
+ }
1320
1322
 
1321
1323
  const pending = readPendingBranches(text)
1322
1324
  if (pending.length > 0) {
@@ -8,7 +8,7 @@
8
8
  - <Verb> <specific component/file/function>
9
9
 
10
10
  <!-- ## Technical context (optional, omit for docs/config/trivial changes) -->
11
- <!-- - <Why this approach was taken, not what changed> -->
11
+ <!-- **<Decision in a few words>.** <Why this approach, what the code can't show, and any shortcoming> -->
12
12
 
13
13
  ## Testing
14
14
 
@@ -69,7 +69,9 @@ A project installed before the board became a folder still holds `.claude/TASKS.
69
69
  - Keep only what is project-specific in the project file, since project settings layer over user settings
70
70
  - Expect `permissions.deny` to hold `Agent` and `Task`, so no session spawns a subagent and parallel work runs as separate background sessions
71
71
  - Expect `autoMemoryEnabled` to be `false`. A person turns it back on for themselves in `settings.local.json`.
72
- - Run `canon migrate session-defaults` on a target seeded before those two defaults, since sync never rewrites an existing settings file
72
+ - Run `canon migrate session-defaults` on a target seeded before those two defaults or before the merge entries below, since sync never rewrites an existing settings file
73
+
74
+ The seed's deny list also holds `Bash(canon pr merge:*)` and `Bash(gh pr merge:*)`, so the operator alone merges. An existing target gets both from `canon migrate session-defaults --write`, and `canon claude sync` warns while either is absent. Neither covers more than the form an agent writes by default.
73
75
 
74
76
  ## User settings
75
77
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "autoMemoryEnabled": false,
3
3
  "permissions": {
4
- "deny": ["Agent", "Task"]
4
+ "deny": ["Agent", "Task", "Bash(canon pr merge:*)", "Bash(gh pr merge:*)"]
5
5
  },
6
6
  "hooks": {
7
7
  "PreToolUse": [