@erclx/canon 4.29.0 → 4.29.1
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/claude-orchestrate/references/orchestrator-dispatch.md +4 -2
- package/claude/skills/claude-planner/REQUIREMENT.md +2 -2
- package/claude/skills/claude-planner/SKILL.md +2 -2
- package/docs/agents/key-changes.md +11 -11
- package/docs/workflow/operating-model.md +1 -1
- package/package.json +1 -1
- package/src/pr/paths.ts +88 -8
|
@@ -249,8 +249,10 @@ over a plan nobody has written yet.
|
|
|
249
249
|
|
|
250
250
|
What a planning dispatch owes instead is the reverse reading, because the plan
|
|
251
251
|
it produces carries a constraint per track in flight and a row planned during a
|
|
252
|
-
wave is planned against a tree that wave is changing. `claude-planner`
|
|
253
|
-
|
|
252
|
+
wave is planned against a tree that wave is changing. `claude-planner` composes
|
|
253
|
+
the session roster with the pull request list for that read rather than reading
|
|
254
|
+
pull requests alone, which is why the brief carries no branch list for it: the
|
|
255
|
+
planner takes this reading itself either way.
|
|
254
256
|
|
|
255
257
|
One row per dispatch. A session reused across a batch pays the context load once
|
|
256
258
|
and ages its picture of the tree while it works, which is what puts the in-flight
|
|
@@ -50,7 +50,7 @@ four carried at least one, caught only because the planner ran
|
|
|
50
50
|
## Must
|
|
51
51
|
|
|
52
52
|
- Assert what a planning session is, what it may not write, and how long the role lasts, since `claude-feature` carries the procedure and no body carries the role
|
|
53
|
-
- State the in-flight read as a command
|
|
53
|
+
- State the in-flight read as a command composing the session roster with open pull requests, and say that a branch and a worktree are not evidence, since a count of either reported merged work as live
|
|
54
54
|
- Run that read once per task rather than once per batch, since a reused session ages its picture of the tree while it works
|
|
55
55
|
- Name every read a plan needed and a launch string did not carry, the task file's findings and the source files among them, since a count quoted from a row was wrong or stale in ten places across four plans
|
|
56
56
|
- Name the validate and audit calls the written plan passes through, since nothing else opens a plan and the audit caught ban hits in three of four trial plans
|
|
@@ -73,7 +73,7 @@ four carried at least one, caught only because the planner ran
|
|
|
73
73
|
## Guards
|
|
74
74
|
|
|
75
75
|
- Task file does not resolve from where the session stands: report it unreadable and name the main-root path, rather than reporting the row as absent
|
|
76
|
-
- A read of what is in flight returns nothing: report that no pull request is open rather than falling back to a branch or worktree count
|
|
76
|
+
- A read of what is in flight returns nothing: report that no pull request is open and no live session holds another branch, rather than falling back to a raw branch or worktree count
|
|
77
77
|
- Launch named no controlling session and an operator is present: ask which row to address rather than inferring
|
|
78
78
|
- Launch named no controlling session and nobody is present: infer from the sessions holding no feature branch, never from a name prefix, and say the addressee was inferred
|
|
79
79
|
|
|
@@ -39,8 +39,8 @@ Each item below is something a plan needed and a launch string did not carry.
|
|
|
39
39
|
|
|
40
40
|
## Read what is in flight rather than inferring it
|
|
41
41
|
|
|
42
|
-
- Run `gh pr list --json number,headRefName`, then `git diff --name-only main...<
|
|
43
|
-
- A branch
|
|
42
|
+
- Run `canon sessions list --json`, take the `branch` field of every session whose `repository` matches this one and whose branch is neither null nor the trunk branch, union that with every `headRefName` from `gh pr list --json number,headRefName`, dedupe by branch name, then diff each with `git diff --name-only main...<branch>` for its file set. That resolves locally whether or not a pull request exists, since every track in this repository shares one git directory across its worktrees.
|
|
43
|
+
- A bare branch or worktree with neither a live session nor a pull request behind it is still not evidence, since this repository squash-merges and leaves both behind. The roster read is already filtered to live sessions, so what changed is the second source composed with it rather than a raw count. It still misses a live session that reached its worktree through the direct-path fallback rather than `EnterWorktree`, since that session's registered branch never moves off the trunk.
|
|
44
44
|
- Run the read once per task rather than once per batch. A session planning several rows ages its picture of the tree while it works, and this read is what dates it.
|
|
45
45
|
- Name each in-flight set as a constraint, say which act it forbids, and stamp the block with the commit the tree was read at, per Constraints in the plan standard. A bare path list leaves the worker guessing.
|
|
46
46
|
|
|
@@ -53,17 +53,17 @@ The cut used to decide whether a span was read at all, and the paths past the co
|
|
|
53
53
|
|
|
54
54
|
A span anywhere in the bullet has to survive all of these:
|
|
55
55
|
|
|
56
|
-
| Rule
|
|
57
|
-
|
|
|
58
|
-
| No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^`
|
|
59
|
-
| No `://` and no leading `/`, `~`, `@`, `#`, `!`
|
|
60
|
-
| Contains `/`
|
|
61
|
-
| Not a dotted-decimal segment, or ends with `/`
|
|
62
|
-
| Not a single top-level folder
|
|
63
|
-
| A `file:line` span leads its bullet
|
|
64
|
-
| The region carries no no-change marker
|
|
65
|
-
|
|
66
|
-
The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described
|
|
56
|
+
| Rule | What it keeps out |
|
|
57
|
+
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
58
|
+
| No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^` | A backticked command, a placeholder, a glob, a pattern |
|
|
59
|
+
| No `://` and no leading `/`, `~`, `@`, `#`, `!` | A URL, an absolute path, a module alias |
|
|
60
|
+
| Contains `/` | A bare filename with no folder around it |
|
|
61
|
+
| Not a dotted-decimal segment, or ends with `/` | A dotted number such as an address |
|
|
62
|
+
| Not a single top-level folder | `src/`, which nobody claims to have rewritten whole |
|
|
63
|
+
| A `file:line` span leads its bullet, or the bullet carries no bare line-only companion | A citation into a file the bullet already named, such as `verify.sh:634` beside `:642` |
|
|
64
|
+
| The region carries no no-change marker | A bullet recording a file it deliberately left alone |
|
|
65
|
+
|
|
66
|
+
The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described only when a bare line-only span sits beside it, such as `:642` following `verify.sh:634`. With no such companion the citation names a second, genuinely distinct file and stays a claim. A line citation leading its bullet stays a claim either way, which is how a body names the exact line it rewrote.
|
|
67
67
|
|
|
68
68
|
The no-change marker is the one rule the region cut cannot substitute for. A body writes "Leave `x` untouched, since the decision keeps it" to record a change it declined, and the path sits ahead of the first comma, so a stricter cut would not reach it and a looser one would find more. Since `keep` and `leave` each open a real claim often enough, the marker decides it rather than the leading verb. Three words carry the set: `untouched`, `unchanged`, and `as written`. `in place` was measured and dropped, because rewriting a file in place is an ordinary claim, and `no other line` was dropped because a correct bullet writes "as one insertion that touches no other line" about a change it did make. `alone` shipped in the set too, until review found every corpus occurrence sitting past the first comma, where the cut already excludes it, so the word voided no true claim there. Restrictive use is the more common one in this repository's own prose, and a comma-free bullet exposed it: "Move the threshold read into `src/gate/stages.ts` alone" asserts an edit, and the marker voided it while the word was still in the set. Over the 40-pull-request corpus the rule still voids no true claim.
|
|
69
69
|
|
|
@@ -77,7 +77,7 @@ start, and only a count of the defect's extent decides how big it is.
|
|
|
77
77
|
|
|
78
78
|
One feature travels this path end to end.
|
|
79
79
|
|
|
80
|
-
1. The next feature is planned with `claude-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 `claude-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
|
|
80
|
+
1. The next feature is planned with `claude-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 `claude-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.
|
|
81
81
|
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 `claude-worktree` and `claude-autoship` 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.
|
|
82
82
|
3. Orchestrator reviews the PR with `claude-pr-review` and posts findings to it.
|
|
83
83
|
4. Orchestrator tells the session holding that branch to run `claude-address-review` 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. `claude-pr-review` states that threshold and the heading follows it, so an open heading is itself the signal to send.
|
package/package.json
CHANGED
package/src/pr/paths.ts
CHANGED
|
@@ -54,9 +54,6 @@ export type KeyChangeRead =
|
|
|
54
54
|
/** The longest bullet a claim carries forward, matching the citation sweep. */
|
|
55
55
|
const PREVIEW_LIMIT = 200
|
|
56
56
|
|
|
57
|
-
/** A backticked span, the only carrier this corpus writes a path in. */
|
|
58
|
-
const BACKTICKED = /`([^`\n]+)`/g
|
|
59
|
-
|
|
60
57
|
/** A list item at any indent, in either bullet spelling or as an ordinal. */
|
|
61
58
|
const BULLET = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/
|
|
62
59
|
|
|
@@ -70,6 +67,15 @@ const HEADING = /^(#{1,6})\s+(.+?)\s*$/
|
|
|
70
67
|
*/
|
|
71
68
|
const LINE_SUFFIX = /:\d+(?:-\d+)?$/
|
|
72
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A span that carries a line suffix and nothing else, such as `:642` beside
|
|
72
|
+
* an earlier `verify.sh:634`. Its presence is what tells the citation guard
|
|
73
|
+
* below the bullet is describing lines inside one already-claimed file rather
|
|
74
|
+
* than naming a second file, since a genuinely distinct file never gets cited
|
|
75
|
+
* by a bare line number with nothing in front of the colon.
|
|
76
|
+
*/
|
|
77
|
+
const BARE_LINE = /^:\d+(?:-\d+)?$/
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* A character that puts the span outside a path this comparison resolves.
|
|
75
81
|
*
|
|
@@ -108,9 +114,73 @@ function isDottedNumber(span: string): boolean {
|
|
|
108
114
|
return /^\d+(?:\.\d+)+$/.test(segment)
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/** One code span, paired by matching backtick-run length rather than count. */
|
|
118
|
+
interface BacktickSpan {
|
|
119
|
+
/** Index of the opening run's first backtick. */
|
|
120
|
+
readonly start: number
|
|
121
|
+
/** Index one past the closing run's last backtick. */
|
|
122
|
+
readonly end: number
|
|
123
|
+
/** The text between the two runs, delimiters excluded. */
|
|
124
|
+
readonly content: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every code span in a bullet, pairing a run of backticks only with the next
|
|
129
|
+
* run of the same length.
|
|
130
|
+
*
|
|
131
|
+
* A single-backtick regex reads a doubled delimiter, such as
|
|
132
|
+
* ``` ``git status`` ```, as two unrelated single backticks: it opens on the
|
|
133
|
+
* second backtick of the pair, closes on the first backtick of the closing
|
|
134
|
+
* pair, and leaves one backtick before and after the span unconsumed. Every
|
|
135
|
+
* scan after that treats a stray leftover backtick as an opener, which
|
|
136
|
+
* absorbs the next real span's opening delimiter as its closer and drops the
|
|
137
|
+
* path inside past it. Pairing by run length rather than by single backtick
|
|
138
|
+
* keeps a doubled delimiter closed by a doubled delimiter, so nothing after
|
|
139
|
+
* it loses its pairing.
|
|
140
|
+
*
|
|
141
|
+
* An opening run with no same-length run after it is not a delimiter, per
|
|
142
|
+
* CommonMark, so it is left as literal text and the scan resumes at the next
|
|
143
|
+
* run rather than backtracking into the unmatched one.
|
|
144
|
+
*/
|
|
145
|
+
function findBacktickSpans(text: string): BacktickSpan[] {
|
|
146
|
+
const runs = [...text.matchAll(/`+/g)].map((match) => ({
|
|
147
|
+
start: match.index ?? 0,
|
|
148
|
+
length: match[0].length,
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
const spans: BacktickSpan[] = []
|
|
152
|
+
let i = 0
|
|
153
|
+
while (i < runs.length) {
|
|
154
|
+
const open = runs[i]
|
|
155
|
+
const closeIndex = runs.findIndex(
|
|
156
|
+
(run, index) => index > i && run.length === open.length,
|
|
157
|
+
)
|
|
158
|
+
if (closeIndex === -1) {
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const close = runs[closeIndex]
|
|
163
|
+
spans.push({
|
|
164
|
+
start: open.start,
|
|
165
|
+
end: close.start + close.length,
|
|
166
|
+
content: text.slice(open.start + open.length, close.start),
|
|
167
|
+
})
|
|
168
|
+
i = closeIndex + 1
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return spans
|
|
172
|
+
}
|
|
173
|
+
|
|
111
174
|
/** Blanks every backticked span so a cue search never fires inside one. */
|
|
112
175
|
function maskSpans(text: string): string {
|
|
113
|
-
|
|
176
|
+
let out = text
|
|
177
|
+
for (const span of findBacktickSpans(text)) {
|
|
178
|
+
out =
|
|
179
|
+
out.slice(0, span.start) +
|
|
180
|
+
' '.repeat(span.end - span.start) +
|
|
181
|
+
out.slice(span.end)
|
|
182
|
+
}
|
|
183
|
+
return out
|
|
114
184
|
}
|
|
115
185
|
|
|
116
186
|
/**
|
|
@@ -329,10 +399,20 @@ export function extractKeyChangePaths(
|
|
|
329
399
|
if (disclaimsChange(trimmed)) continue
|
|
330
400
|
|
|
331
401
|
const accusesBefore = claimRegion(trimmed).length
|
|
402
|
+
const spans = findBacktickSpans(trimmed)
|
|
403
|
+
// Whether a `file:line` span citing an already-claimed file is a second
|
|
404
|
+
// claim or a citation into the file just claimed turns on this: a bullet
|
|
405
|
+
// that also carries a bare `:642`-shaped span alongside it is describing
|
|
406
|
+
// two lines in one file, where a bullet with no such companion is naming
|
|
407
|
+
// a second, genuinely distinct file. Computed once, up front, so the
|
|
408
|
+
// guard below reads it rather than scanning again for every span.
|
|
409
|
+
const hasBareLineCompanion = spans.some((span) =>
|
|
410
|
+
BARE_LINE.test(span.content),
|
|
411
|
+
)
|
|
332
412
|
let claimed = false
|
|
333
413
|
|
|
334
|
-
for (const match of
|
|
335
|
-
const span = match
|
|
414
|
+
for (const match of spans) {
|
|
415
|
+
const span = match.content
|
|
336
416
|
const bare = span.replace(LINE_SUFFIX, '')
|
|
337
417
|
|
|
338
418
|
// A `file:line` span following another claim in the same bullet is a
|
|
@@ -342,7 +422,7 @@ export function extractKeyChangePaths(
|
|
|
342
422
|
// its bullet it is an ordinary claim, which is how a body names the exact
|
|
343
423
|
// line it rewrote.
|
|
344
424
|
const cited = bare !== span
|
|
345
|
-
if (cited && claimed) continue
|
|
425
|
+
if (cited && claimed && hasBareLineCompanion) continue
|
|
346
426
|
|
|
347
427
|
const resolved = resolveSpan(bare)
|
|
348
428
|
if (resolved === undefined) continue
|
|
@@ -357,7 +437,7 @@ export function extractKeyChangePaths(
|
|
|
357
437
|
path: resolved.path,
|
|
358
438
|
directory: resolved.directory,
|
|
359
439
|
anchored: roots.has(resolved.path.slice(0, resolved.path.indexOf('/'))),
|
|
360
|
-
leading:
|
|
440
|
+
leading: match.start < accusesBefore,
|
|
361
441
|
span,
|
|
362
442
|
bullet: index + 1,
|
|
363
443
|
preview,
|