@erclx/canon 4.18.0 → 4.19.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/docs/agents/commands.md +1 -1
- package/package.json +1 -1
- package/src/commands/labels.ts +23 -4
- package/src/labels/phase.ts +57 -4
- package/standards/pr.md +2 -1
- package/standards/publish.md +10 -0
package/docs/agents/commands.md
CHANGED
|
@@ -68,7 +68,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
68
68
|
| `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
69
69
|
| `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
70
70
|
| `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
71
|
-
| `canon labels scan` | Fail a pull request whose title or body
|
|
71
|
+
| `canon labels scan` | Fail a pull request whose title or body carries a phase label, a label a code span quotes, a gitignored record path, or a session link (`--event`, `--json`) |
|
|
72
72
|
| `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
|
|
73
73
|
| `canon pr key-changes` | Compare the files a pull request body's Key Changes names against its own diff, in both directions (`--body`, `--base`, `--json`) |
|
|
74
74
|
| `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
|
package/package.json
CHANGED
package/src/commands/labels.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function register(program: Command): void {
|
|
|
90
90
|
labels
|
|
91
91
|
.command('scan')
|
|
92
92
|
.description(
|
|
93
|
-
'Fail a pull request whose title or body carries a phase label or a
|
|
93
|
+
'Fail a pull request whose title or body carries a phase label, a board identifier, or a session link',
|
|
94
94
|
)
|
|
95
95
|
.helpOption('-h, --help', 'Show this help message')
|
|
96
96
|
.option(
|
|
@@ -118,10 +118,16 @@ export function register(program: Command): void {
|
|
|
118
118
|
'copy of. A path under a tracked folder is left alone, so a rule or a',
|
|
119
119
|
'skill any clone resolves is not reported.',
|
|
120
120
|
'',
|
|
121
|
+
'It reports a session link as the third category, being a link to one',
|
|
122
|
+
'Claude Code session, which the harness appends to text it tells a',
|
|
123
|
+
'session to publish. That link resolves for the one account holding the',
|
|
124
|
+
'session and for no other reader, which no clone repairs, so it is read',
|
|
125
|
+
'on a release pull request too, where the board identifier is not.',
|
|
126
|
+
'',
|
|
121
127
|
'Exit codes:',
|
|
122
|
-
' 0 no phase label
|
|
128
|
+
' 0 no phase label, no board identifier, and no session link found',
|
|
123
129
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
124
|
-
' 2 the title or body carries
|
|
130
|
+
' 2 the title or body carries one of the three',
|
|
125
131
|
'',
|
|
126
132
|
'Examples:',
|
|
127
133
|
' canon labels scan --event "$GITHUB_EVENT_PATH"',
|
|
@@ -363,6 +369,16 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
363
369
|
for (const reference of result.boardReferences) logWarn(reference)
|
|
364
370
|
}
|
|
365
371
|
|
|
372
|
+
logStep(result.sessionLinks.length === 0 ? 'Clean' : 'Session link found')
|
|
373
|
+
if (result.sessionLinks.length === 0) {
|
|
374
|
+
logInfo('no session link in the title or body')
|
|
375
|
+
} else {
|
|
376
|
+
logWarn(
|
|
377
|
+
`${plural(result.sessionLinks.length, 'session link')} in the title or body. Delete every hit rather than rewriting it, since a session resolves for the one account that started it and for no other reader.`,
|
|
378
|
+
)
|
|
379
|
+
for (const link of result.sessionLinks) logWarn(link)
|
|
380
|
+
}
|
|
381
|
+
|
|
366
382
|
outro()
|
|
367
383
|
|
|
368
384
|
if (emitJson) {
|
|
@@ -372,11 +388,14 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
372
388
|
phaseLabels: result.phaseLabels,
|
|
373
389
|
semverTags: result.semverTags,
|
|
374
390
|
boardReferences: result.boardReferences,
|
|
391
|
+
sessionLinks: result.sessionLinks,
|
|
375
392
|
})}\n`,
|
|
376
393
|
)
|
|
377
394
|
}
|
|
378
395
|
|
|
379
|
-
return result.phaseLabels.length === 0 &&
|
|
396
|
+
return result.phaseLabels.length === 0 &&
|
|
397
|
+
result.boardReferences.length === 0 &&
|
|
398
|
+
result.sessionLinks.length === 0
|
|
380
399
|
? 0
|
|
381
400
|
: 2
|
|
382
401
|
}
|
package/src/labels/phase.ts
CHANGED
|
@@ -34,6 +34,19 @@ export interface PhaseScanResult {
|
|
|
34
34
|
* nor the gitignored folder a path names.
|
|
35
35
|
*/
|
|
36
36
|
readonly boardReferences: readonly string[]
|
|
37
|
+
/**
|
|
38
|
+
* A link to one Claude Code session, which the harness appends to text it
|
|
39
|
+
* tells a session to publish.
|
|
40
|
+
*
|
|
41
|
+
* It sits beside the board references rather than inside them because the
|
|
42
|
+
* two are unresolvable for different reasons. A record path fails for a
|
|
43
|
+
* reader holding no copy of this checkout, and a session link fails for
|
|
44
|
+
* everyone outside the one account that holds the session, which no clone
|
|
45
|
+
* and no checkout repairs. The report line for a board reference names a
|
|
46
|
+
* record path and a quoted label, so a session link folded in would be
|
|
47
|
+
* reported under a sentence that does not describe it.
|
|
48
|
+
*/
|
|
49
|
+
readonly sessionLinks: readonly string[]
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
const VERSION_TOKEN = /\bv\d+(?:\.\d+){1,2}\b/g
|
|
@@ -98,6 +111,26 @@ const RECORD_PATH = new RegExp(
|
|
|
98
111
|
'g',
|
|
99
112
|
)
|
|
100
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A link to one Claude Code session, matched on the host and the path segment
|
|
116
|
+
* rather than on the identifier alphabet.
|
|
117
|
+
*
|
|
118
|
+
* The two instances on the trunk carry a 24-character identifier after
|
|
119
|
+
* `session_`, and reading that shape into the pattern would empty this check
|
|
120
|
+
* the moment the harness changed it, with nothing left to report and no
|
|
121
|
+
* failure to notice. The host and the path segment are what the harness has to
|
|
122
|
+
* keep for the link to resolve at all. A host change still gets past this and
|
|
123
|
+
* nothing detects that.
|
|
124
|
+
*
|
|
125
|
+
* The scheme is optional because it is incidental to the two parts being
|
|
126
|
+
* matched, so a link written without it is the same unresolvable reference.
|
|
127
|
+
* The tail runs to the first whitespace or closing delimiter, which is how a
|
|
128
|
+
* record path is read, so a report names the whole link rather than the prefix
|
|
129
|
+
* that matched.
|
|
130
|
+
*/
|
|
131
|
+
const SESSION_LINK =
|
|
132
|
+
/(?<![\w./-])(?:https?:\/\/)?claude\.ai\/code\/session_[^\s`)\]]+/g
|
|
133
|
+
|
|
101
134
|
/**
|
|
102
135
|
* The head branch release-please opens every release pull request under.
|
|
103
136
|
*
|
|
@@ -180,6 +213,16 @@ function recordPaths(text: string): string[] {
|
|
|
180
213
|
]
|
|
181
214
|
}
|
|
182
215
|
|
|
216
|
+
function sessionLinks(text: string): string[] {
|
|
217
|
+
return [
|
|
218
|
+
...new Set(
|
|
219
|
+
(text.match(SESSION_LINK) ?? []).map((link) =>
|
|
220
|
+
link.replace(TRAILING_PUNCTUATION, ''),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
|
|
183
226
|
/**
|
|
184
227
|
* Reads a title and a body for version-shaped tokens and sorts every one
|
|
185
228
|
* found into the namespace this pull request is allowed to carry.
|
|
@@ -197,11 +240,22 @@ function recordPaths(text: string): string[] {
|
|
|
197
240
|
* body without passing the gate on its own. That its author has nothing to
|
|
198
241
|
* rewrite is true as well and is the weaker half, since it would leave the
|
|
199
242
|
* reference standing and unresolvable.
|
|
243
|
+
*
|
|
244
|
+
* A session link is reported on both paths, the release one included. Nothing
|
|
245
|
+
* appends one to a release body, which release-please generates with no
|
|
246
|
+
* session in the loop, so populating the field there costs nothing on every
|
|
247
|
+
* run this repository has seen. What it buys is that the coverage argument
|
|
248
|
+
* above never has to hold for this category: that argument reasons from every
|
|
249
|
+
* commit in the generated history having passed this gate, and a link the gate
|
|
250
|
+
* did not yet scan for would reach a release body under it. Reading the whole
|
|
251
|
+
* body on both paths leaves no hole if the premise ever slips.
|
|
200
252
|
*/
|
|
201
253
|
export function scanPhaseLabels(input: PhaseScanInput): PhaseScanResult {
|
|
202
254
|
const source = `${input.title}\n${input.body}`
|
|
255
|
+
const outsideFences = linesOutsideFences(source).join('\n')
|
|
203
256
|
const tokens = versionTokens(readable(source))
|
|
204
257
|
const cutsRelease = isReleasePullRequest(input)
|
|
258
|
+
const links = sessionLinks(outsideFences)
|
|
205
259
|
|
|
206
260
|
if (cutsRelease) {
|
|
207
261
|
return {
|
|
@@ -209,6 +263,7 @@ export function scanPhaseLabels(input: PhaseScanInput): PhaseScanResult {
|
|
|
209
263
|
phaseLabels: [],
|
|
210
264
|
semverTags: tokens,
|
|
211
265
|
boardReferences: [],
|
|
266
|
+
sessionLinks: links,
|
|
212
267
|
}
|
|
213
268
|
}
|
|
214
269
|
|
|
@@ -222,9 +277,7 @@ export function scanPhaseLabels(input: PhaseScanInput): PhaseScanResult {
|
|
|
222
277
|
cutsRelease,
|
|
223
278
|
phaseLabels: tokens,
|
|
224
279
|
semverTags: [],
|
|
225
|
-
boardReferences: [
|
|
226
|
-
|
|
227
|
-
...recordPaths(linesOutsideFences(source).join('\n')),
|
|
228
|
-
],
|
|
280
|
+
boardReferences: [...quoted, ...recordPaths(outsideFences)],
|
|
281
|
+
sessionLinks: links,
|
|
229
282
|
}
|
|
230
283
|
}
|
package/standards/pr.md
CHANGED
|
@@ -14,7 +14,7 @@ Does not govern:
|
|
|
14
14
|
- Commit subject format, which shares the title form: `commit.md`
|
|
15
15
|
- Branch naming: `branch.md`
|
|
16
16
|
- Whether a phase label or a semver tag may appear in a title or body: `versioning.md`
|
|
17
|
-
- Whether a quoted label
|
|
17
|
+
- Whether a quoted label, a gitignored record path, or a link to one Claude Code session may appear in a title or body, which `canon labels scan` fails on: `publish.md`
|
|
18
18
|
- Voice, rhythm, and sentence construction in pull request prose: the `write-human` skill
|
|
19
19
|
- Punctuation, formatting, and banned words in pull request prose: `markdown.md`
|
|
20
20
|
|
|
@@ -46,6 +46,7 @@ Does not govern:
|
|
|
46
46
|
- Use checkboxes, never prose. See Testing discipline for which box gets ticked.
|
|
47
47
|
- `## For the reviewer` (optional): what the reviewing session should confirm, one bullet per request
|
|
48
48
|
- Visuals: include only when they clarify architecture, UI, or complex logic flows
|
|
49
|
+
- The list is closed. A body carries the sections above and nothing after the last of them, which covers a trailer a harness appends once the body is composed. `publish.md` states the rule, and `canon labels scan` fails on the one such trailer measured so far.
|
|
49
50
|
|
|
50
51
|
## Testing discipline
|
|
51
52
|
|
package/standards/publish.md
CHANGED
|
@@ -46,6 +46,16 @@ Rewrite a hit to name what the reader can reach rather than deleting it. A row's
|
|
|
46
46
|
|
|
47
47
|
`canon labels scan` runs this check and the phase-label one over a pull request title and body. It reads that pair alone, so every other channel is the author's own scan.
|
|
48
48
|
|
|
49
|
+
## Session links
|
|
50
|
+
|
|
51
|
+
A link to one Claude Code session names the session that wrote the text rather than the change the text describes. It enters because the harness tells the composing session to end its published text with one, so the author writes it deliberately and it reads as required rather than as a slip. That is what carries it past a scan the same author runs, and it is why a rule stating the ban does not reach the case: the session is following an instruction it was given, and reads its own compliance as correct.
|
|
52
|
+
|
|
53
|
+
This check depends on the reader holding the account rather than the checkout, which is narrower than the dependency the destination rule above scopes. A reader inside this checkout opens a record path and still cannot open a session, since the session resolves for the one account that started it and for nobody else. The scoping still sits at the invocation site, because `canon labels scan` reads a pull request title and body and reaches no other text.
|
|
54
|
+
|
|
55
|
+
Delete a hit rather than rewriting it. The other two checks replace a hit with what the reader can reach instead, and a session has no such form, so what stands in its place is the body already written above it.
|
|
56
|
+
|
|
57
|
+
Removing the link before a merge does not substitute for never writing it. One branch removed it, read the live body back clean, and the squashed commit landed carrying it anyway off an earlier snapshot, which is permanent on the trunk.
|
|
58
|
+
|
|
49
59
|
## Cross-reference form
|
|
50
60
|
|
|
51
61
|
A number referring to a pull request or an issue takes the form its destination renders. Write it bare where the destination auto-links it, and in backticks where it does not. Both spellings are correct, each in one place, so a reference moved from one destination to the other is rewritten rather than copied.
|