@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-pr/REQUIREMENT.md +2 -2
- package/claude/skills/git-pr/SKILL.md +2 -1
- package/claude/skills/git-pr/references/closing-issues.md +2 -0
- package/claude/skills/git-pr/references/pr.md +58 -7
- package/claude/skills/review-address/SKILL.md +18 -6
- package/claude/skills/review-address/references/post-review-findings.md +4 -1
- package/claude/skills/review-address/references/rebase-only.md +14 -2
- package/claude/skills/review-pr/SKILL.md +9 -2
- package/claude/skills/review-pr/references/body-variants.md +29 -8
- package/claude/skills/review-pr/references/post.md +29 -10
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +4 -3
- package/claude/skills/review-ui/references/post.md +16 -2
- package/claude/skills/review-ui/references/tick.md +7 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +4 -2
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +4 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +1 -0
- package/claude/skills/task-board/references/decline.md +2 -2
- package/claude/skills/task-board/references/tasks.md +4 -2
- package/claude/skills/task-complete/SKILL.md +1 -0
- package/claude/skills/task-complete/references/archive.md +5 -3
- package/claude/skills/ui-checklist/SKILL.md +3 -3
- package/docs/agents/migrate.md +2 -2
- package/docs/target/migrations.md +4 -2
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/zshrc-aliases.md +3 -0
- package/package.json +1 -1
- package/scripts/core/bootstrap.sh +1 -0
- package/src/commands/claude.ts +4 -0
- package/src/commands/labels.ts +66 -1
- package/src/commands/migrate.ts +5 -4
- package/src/commands/pr.ts +405 -30
- package/src/commands/sessions.ts +25 -11
- package/src/commands/tasks.ts +11 -1
- package/src/labels/density.ts +363 -0
- package/src/migrate/session-defaults.ts +20 -5
- package/src/pr/evidence.ts +1 -1
- package/src/pr/lift.ts +19 -0
- package/src/pr/merge.ts +175 -0
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +40 -1
- package/src/pr/tick.ts +109 -0
- package/src/sessions/live.ts +26 -0
- package/src/sessions/resolve.ts +62 -11
- package/src/tasks/archive.ts +32 -8
- package/src/tasks/closes.ts +9 -5
- package/src/tasks/validate.ts +4 -2
- package/tooling/base/seeds/.github/pull_request_template.md +1 -1
- package/tooling/claude/reference.md +3 -1
- package/tooling/claude/seeds/.claude/settings.json +1 -1
package/src/tasks/archive.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/tasks/closes.ts
CHANGED
|
@@ -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. */
|
package/src/tasks/validate.ts
CHANGED
|
@@ -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)
|
|
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
|
-
<!--
|
|
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
|
|