@erclx/canon 5.45.0 → 5.46.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/api-design/REQUIREMENT.md +15 -31
- package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
- package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
- package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
- package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
- package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
- package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
- package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
- package/claude/skills/code-craft/REQUIREMENT.md +14 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
- package/claude/skills/context-fold/REQUIREMENT.md +15 -39
- package/claude/skills/context-fold/references/context.md +3 -3
- package/claude/skills/create-rule/REQUIREMENT.md +10 -24
- package/claude/skills/create-rule/SKILL.md +3 -0
- package/claude/skills/create-skill/REQUIREMENT.md +10 -21
- package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
- package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
- package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
- package/claude/skills/design-identity/REQUIREMENT.md +14 -20
- package/claude/skills/design-taste/REQUIREMENT.md +16 -57
- package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
- package/claude/skills/document-health/REQUIREMENT.md +15 -29
- package/claude/skills/document-health/SKILL.md +2 -2
- package/claude/skills/document-repair/REQUIREMENT.md +32 -0
- package/claude/skills/document-repair/SKILL.md +76 -0
- package/claude/skills/document-repair/references/batches.md +44 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
- package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
- package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
- package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
- package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
- package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
- package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
- package/claude/skills/git-branch/REQUIREMENT.md +12 -26
- package/claude/skills/git-commit/REQUIREMENT.md +11 -15
- package/claude/skills/git-followup/REQUIREMENT.md +11 -22
- package/claude/skills/git-issue/REQUIREMENT.md +9 -15
- package/claude/skills/git-pr/REQUIREMENT.md +13 -32
- package/claude/skills/git-pr/SKILL.md +10 -2
- package/claude/skills/git-pr/references/closing-issues.md +47 -0
- package/claude/skills/git-pr/references/pr.md +4 -0
- package/claude/skills/git-ship/REQUIREMENT.md +13 -26
- package/claude/skills/git-split/REQUIREMENT.md +10 -17
- package/claude/skills/git-stage/REQUIREMENT.md +12 -18
- package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
- package/claude/skills/make-deck/REQUIREMENT.md +13 -21
- package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
- package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
- package/claude/skills/plan-groundwork/SKILL.md +5 -2
- package/claude/skills/plan-groundwork/references/output.md +47 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
- package/claude/skills/plan-intake/references/intake.md +2 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
- package/claude/skills/plan-intake-answer/SKILL.md +7 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
- package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
- package/claude/skills/read-frames/REQUIREMENT.md +3 -8
- package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
- package/claude/skills/review-address/REQUIREMENT.md +14 -38
- package/claude/skills/review-craft/REQUIREMENT.md +14 -32
- package/claude/skills/review-pr/REQUIREMENT.md +15 -34
- package/claude/skills/review-ui/REQUIREMENT.md +14 -39
- package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
- package/claude/skills/role-reviewer/SKILL.md +5 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
- package/claude/skills/search-craft/REQUIREMENT.md +12 -36
- package/claude/skills/search-craft/SKILL.md +1 -1
- package/claude/skills/search-craft/references/adopted.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +13 -23
- package/claude/skills/session-map/REQUIREMENT.md +13 -29
- package/claude/skills/session-relay/REQUIREMENT.md +14 -58
- package/claude/skills/session-relay/SKILL.md +6 -0
- package/claude/skills/session-resume/REQUIREMENT.md +13 -27
- package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
- package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
- package/claude/skills/target-check/REQUIREMENT.md +16 -23
- package/claude/skills/target-setup/REQUIREMENT.md +11 -34
- package/claude/skills/task-board/REQUIREMENT.md +13 -22
- package/claude/skills/task-board/SKILL.md +7 -0
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/REQUIREMENT.md +11 -24
- package/claude/skills/task-complete/references/archive.md +1 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
- package/claude/skills/test-craft/REQUIREMENT.md +15 -46
- package/claude/skills/test-first/REQUIREMENT.md +14 -20
- package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
- package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
- package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
- package/claude/skills/video-craft/REQUIREMENT.md +14 -27
- package/claude/skills/write-doc/REQUIREMENT.md +14 -24
- package/claude/skills/write-human/REQUIREMENT.md +15 -27
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
- package/docs/agents/context-audit.md +19 -12
- package/docs/agents/markdown-audit.md +15 -10
- package/docs/target/projects.md +2 -0
- package/docs/workflow/skill-map.md +6 -5
- package/governance/renames.toml +0 -2
- package/governance/rules/claude/output.md +5 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/stacks/base.toml +0 -1
- package/package.json +1 -1
- package/src/claude/cases/workflow.ts +12 -0
- package/src/commands/context.ts +57 -3
- package/src/commands/labels.ts +83 -3
- package/src/commands/markdown.ts +188 -21
- package/src/commands/sessions.ts +36 -3
- package/src/commands/tasks.ts +180 -0
- package/src/commands/worktrees.ts +2 -0
- package/src/context/coverage.ts +210 -0
- package/src/context/gate.ts +12 -3
- package/src/gate/stages.ts +1 -1
- package/src/init/steps.ts +45 -0
- package/src/intake/items.ts +14 -0
- package/src/labels/closing.ts +46 -0
- package/src/markdown/gated.ts +250 -0
- package/src/markdown/scope.ts +115 -2
- package/src/migrate/rename.ts +6 -0
- package/src/sessions/claim.ts +7 -0
- package/src/sessions/live.ts +53 -6
- package/src/sessions/registry.ts +11 -0
- package/src/sessions/resolve.ts +32 -5
- package/src/tasks/closes.ts +136 -0
- package/src/tasks/record.ts +1 -1
- package/src/worktrees/reclaim.ts +16 -1
- package/tooling/base/configs/scripts/verify.ts +1 -1
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/governance/rules/canon/task-scope.md +0 -12
package/src/commands/markdown.ts
CHANGED
|
@@ -29,7 +29,16 @@ import {
|
|
|
29
29
|
bodyLines,
|
|
30
30
|
scanBans,
|
|
31
31
|
} from '@/markdown/scan'
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
appendGatedPrefix,
|
|
34
|
+
findingsUnder,
|
|
35
|
+
GATED_CONFIG_REL,
|
|
36
|
+
type GatedList,
|
|
37
|
+
prefixShapeError,
|
|
38
|
+
readGatedConfig,
|
|
39
|
+
resolveGatedList,
|
|
40
|
+
} from '@/markdown/gated'
|
|
41
|
+
import { densityGates, inDensityScope } from '@/markdown/scope'
|
|
33
42
|
import {
|
|
34
43
|
BASELINE,
|
|
35
44
|
CHECKPOINTS,
|
|
@@ -49,6 +58,9 @@ import {
|
|
|
49
58
|
} from '@/cli/ui'
|
|
50
59
|
import { isOwnCheckout } from '@/roots/project'
|
|
51
60
|
|
|
61
|
+
/** A config the audit cannot read refuses rather than gating nothing. */
|
|
62
|
+
type AuditRefusal = MarkdownAuditRefusal | 'malformed-config'
|
|
63
|
+
|
|
52
64
|
const EXIT_REFUSED = 1
|
|
53
65
|
const EXIT_GATE = 2
|
|
54
66
|
|
|
@@ -119,9 +131,12 @@ export function register(program: Command): void {
|
|
|
119
131
|
'a row, a 200-character bullet, 5 plain bullets in a row, no H4, and a',
|
|
120
132
|
'paragraph or table opening every H3. Only context, docs, skills, tooling',
|
|
121
133
|
'seeds and references, the wiki, README.md, and CONTRIBUTING.md take a',
|
|
122
|
-
'finding.
|
|
123
|
-
'and a bullet run gates in context, docs, and
|
|
124
|
-
'
|
|
134
|
+
'finding. In a canon checkout one gates under a prefix in GATED_PATHS in',
|
|
135
|
+
'src/markdown/scope.ts, and a bullet run gates in context, docs, and',
|
|
136
|
+
'skills from the start. Elsewhere one gates only under a prefix',
|
|
137
|
+
'canon/config/markdown.toml lists, which canon markdown gated add writes,',
|
|
138
|
+
'and with no such file every density finding reports and none gates.',
|
|
139
|
+
'A config that does not parse refuses with its path named.',
|
|
125
140
|
'',
|
|
126
141
|
'A page whose frontmatter carries kind: reference takes the reference',
|
|
127
142
|
'shape: no H2 beyond Output, Refusals, and See also, each at most once',
|
|
@@ -165,6 +180,131 @@ export function register(program: Command): void {
|
|
|
165
180
|
.action(async (paths: string[], opts: AuditCommandOptions) => {
|
|
166
181
|
process.exitCode = await runAudit(paths, opts)
|
|
167
182
|
})
|
|
183
|
+
|
|
184
|
+
const gated = markdown
|
|
185
|
+
.command('gated')
|
|
186
|
+
.description("Read and extend a project's gated density prefixes")
|
|
187
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
188
|
+
|
|
189
|
+
gated
|
|
190
|
+
.command('add')
|
|
191
|
+
.description(
|
|
192
|
+
'Gate a folder or file once it carries no density or reference shape finding',
|
|
193
|
+
)
|
|
194
|
+
.argument(
|
|
195
|
+
'<prefix>',
|
|
196
|
+
'A folder ending in a slash, or the path of one existing file',
|
|
197
|
+
)
|
|
198
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
199
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
200
|
+
.addHelpText(
|
|
201
|
+
'after',
|
|
202
|
+
[
|
|
203
|
+
'',
|
|
204
|
+
'Exit codes:',
|
|
205
|
+
' 0 the prefix is listed, appended now or already present',
|
|
206
|
+
' 1 refused, with the reason on stderr, and the config left untouched',
|
|
207
|
+
'',
|
|
208
|
+
'Appends the prefix to canon/config/markdown.toml only when every file',
|
|
209
|
+
'under it carries zero findings that would gate once it is listed, so a',
|
|
210
|
+
'gated folder never fails a push the moment it is added. A folder holding',
|
|
211
|
+
'no markdown file passes. A prefix without a trailing slash would also',
|
|
212
|
+
'match a sibling such as docs-old/, so one must name an existing file.',
|
|
213
|
+
"A skill's REQUIREMENT.md gates only on its own path, never a folder.",
|
|
214
|
+
'',
|
|
215
|
+
"Canon's own checkout lists its prefixes in GATED_PATHS in",
|
|
216
|
+
'src/markdown/scope.ts, so the verb refuses there.',
|
|
217
|
+
'',
|
|
218
|
+
'Examples:',
|
|
219
|
+
' canon markdown gated add docs/',
|
|
220
|
+
' canon markdown gated add .claude/skills/my-skill/REQUIREMENT.md --json',
|
|
221
|
+
'',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
)
|
|
224
|
+
.action(async (prefix: string, opts: AuditCommandOptions) => {
|
|
225
|
+
process.exitCode = await runGatedAdd(prefix, opts)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type GatedAddRefusal =
|
|
230
|
+
| 'canon-checkout'
|
|
231
|
+
| 'malformed-config'
|
|
232
|
+
| 'bad-prefix'
|
|
233
|
+
| 'no-git'
|
|
234
|
+
| 'findings'
|
|
235
|
+
|
|
236
|
+
async function runGatedAdd(
|
|
237
|
+
prefix: string,
|
|
238
|
+
opts: AuditCommandOptions,
|
|
239
|
+
): Promise<number> {
|
|
240
|
+
const root = process.cwd()
|
|
241
|
+
const json = opts.json ?? false
|
|
242
|
+
const refuseAdd = (
|
|
243
|
+
reason: GatedAddRefusal,
|
|
244
|
+
message: string,
|
|
245
|
+
extra: Record<string, unknown> = {},
|
|
246
|
+
): number => {
|
|
247
|
+
intro('canon markdown gated add')
|
|
248
|
+
logStep('Refused')
|
|
249
|
+
logWarn(message)
|
|
250
|
+
outro()
|
|
251
|
+
if (json) {
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
`${JSON.stringify({ root, prefix, reason, message, ...extra })}\n`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
return EXIT_REFUSED
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isOwnCheckout(root)) {
|
|
260
|
+
return refuseAdd(
|
|
261
|
+
'canon-checkout',
|
|
262
|
+
'This is a canon checkout, which gates through GATED_PATHS in src/markdown/scope.ts. Add the prefix there.',
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const config = readGatedConfig(root)
|
|
267
|
+
if (config.kind === 'malformed') {
|
|
268
|
+
return refuseAdd('malformed-config', config.message)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const shape = prefixShapeError(root, prefix)
|
|
272
|
+
if (shape !== undefined) return refuseAdd('bad-prefix', shape)
|
|
273
|
+
|
|
274
|
+
const findings = await findingsUnder(root, prefix)
|
|
275
|
+
if (!findings) {
|
|
276
|
+
return refuseAdd(
|
|
277
|
+
'no-git',
|
|
278
|
+
'git could not list the tree, so nothing under the prefix was measured. Run inside a git repository.',
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
if (findings.length > 0) {
|
|
282
|
+
return refuseAdd(
|
|
283
|
+
'findings',
|
|
284
|
+
`${plural(findings.length, 'finding')} under ${prefix} would gate once it is listed. Run 'canon markdown audit ${prefix}' and bring it to zero first.`,
|
|
285
|
+
{ findings },
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const current = config.kind === 'listed' ? config.prefixes : []
|
|
290
|
+
const action = appendGatedPrefix(root, current, prefix)
|
|
291
|
+
|
|
292
|
+
intro('canon markdown gated add')
|
|
293
|
+
logStep(action === 'appended' ? 'Appended' : 'Already listed')
|
|
294
|
+
logInfo(
|
|
295
|
+
action === 'appended'
|
|
296
|
+
? `${prefix} now gates in ${GATED_CONFIG_REL}.`
|
|
297
|
+
: `${prefix} was already listed in ${GATED_CONFIG_REL}, so nothing was written.`,
|
|
298
|
+
)
|
|
299
|
+
outro()
|
|
300
|
+
|
|
301
|
+
if (json) {
|
|
302
|
+
process.stdout.write(
|
|
303
|
+
`${JSON.stringify({ root, prefix, action, config: GATED_CONFIG_REL })}\n`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return 0
|
|
168
308
|
}
|
|
169
309
|
|
|
170
310
|
async function runAudit(
|
|
@@ -194,12 +334,20 @@ async function runAudit(
|
|
|
194
334
|
)
|
|
195
335
|
}
|
|
196
336
|
|
|
337
|
+
const resolution = resolveGatedList(root)
|
|
338
|
+
if (resolution.kind === 'malformed') {
|
|
339
|
+
return refuse(
|
|
340
|
+
'malformed-config',
|
|
341
|
+
resolution.message,
|
|
342
|
+
root,
|
|
343
|
+
opts.json ?? false,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
const gated = resolution.list
|
|
347
|
+
|
|
197
348
|
const bans = BAN_SETS
|
|
198
349
|
const empty = emptyBanSets(bans)
|
|
199
350
|
const checkpoints = CHECKPOINTS
|
|
200
|
-
// `GATED_PATHS` describes this repository's rewrite progress, so a target
|
|
201
|
-
// reads every density finding as a report until its own corpus is gated.
|
|
202
|
-
const gates = isOwnCheckout(root)
|
|
203
351
|
|
|
204
352
|
const reports: FileReport[] = await Promise.all(
|
|
205
353
|
scope.files.map(async (rel) => {
|
|
@@ -216,12 +364,17 @@ async function runAudit(
|
|
|
216
364
|
density: inDensityScope(rel)
|
|
217
365
|
? densityFindings(lines).map((finding) => ({
|
|
218
366
|
...finding,
|
|
219
|
-
gating:
|
|
367
|
+
gating: densityGates(
|
|
368
|
+
rel,
|
|
369
|
+
finding.kind,
|
|
370
|
+
gated.prefixes,
|
|
371
|
+
gated.alwaysGated,
|
|
372
|
+
),
|
|
220
373
|
}))
|
|
221
374
|
: [],
|
|
222
375
|
referenceShape: referenceShapeFindings(source).map((finding) => ({
|
|
223
376
|
...finding,
|
|
224
|
-
gating:
|
|
377
|
+
gating: referenceShapeGates(rel, gated.prefixes),
|
|
225
378
|
})),
|
|
226
379
|
links: findBrokenLinks(lines, (path) =>
|
|
227
380
|
existsSync(resolve(dirname(abs), path)),
|
|
@@ -239,8 +392,8 @@ async function runAudit(
|
|
|
239
392
|
reportLinks(reports, root)
|
|
240
393
|
reportColons(reports)
|
|
241
394
|
reportHeadingCase(reports)
|
|
242
|
-
reportDensity(reports,
|
|
243
|
-
reportReferenceShape(reports,
|
|
395
|
+
reportDensity(reports, gated, resolution.source)
|
|
396
|
+
reportReferenceShape(reports, gated)
|
|
244
397
|
reportBullets(reports, checkpoints)
|
|
245
398
|
reportParagraphs(reports, checkpoints)
|
|
246
399
|
reportCadence(reports, checkpoints)
|
|
@@ -308,7 +461,7 @@ async function runAudit(
|
|
|
308
461
|
}
|
|
309
462
|
|
|
310
463
|
function refuse(
|
|
311
|
-
reason:
|
|
464
|
+
reason: AuditRefusal,
|
|
312
465
|
message: string,
|
|
313
466
|
root: string,
|
|
314
467
|
emitJson: boolean,
|
|
@@ -480,7 +633,11 @@ function describeDensity(finding: GatedDensityFinding): string {
|
|
|
480
633
|
* under no gated prefix reports the same finding a gated one fails on and a
|
|
481
634
|
* reader has to tell the two apart in one frame.
|
|
482
635
|
*/
|
|
483
|
-
function reportDensity(
|
|
636
|
+
function reportDensity(
|
|
637
|
+
reports: readonly FileReport[],
|
|
638
|
+
gated: GatedList,
|
|
639
|
+
source: 'canon' | 'config' | 'absent',
|
|
640
|
+
): void {
|
|
484
641
|
logStep('Density')
|
|
485
642
|
logInfo(
|
|
486
643
|
`Paragraphs past ${CHECKPOINTS.paragraph} characters, more than 2 paragraphs in a row, bullets past ${CHECKPOINTS.bullet} characters, more than 5 plain bullets in a row, a heading past H3, and an H3 opening with neither a paragraph nor a table.`,
|
|
@@ -488,11 +645,7 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
|
488
645
|
logInfo(
|
|
489
646
|
'Measured on context entries, docs, skill bodies, references, and requirements, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. Every other path takes no density finding.',
|
|
490
647
|
)
|
|
491
|
-
logInfo(
|
|
492
|
-
gates
|
|
493
|
-
? `A finding gates under a gated prefix (${GATED_PATHS.length === 0 ? 'none yet' : GATED_PATHS.join(', ')}), and a bullet run gates in context, docs, and skills already. Every other finding reports.`
|
|
494
|
-
: 'This tree is not a canon checkout, so every density finding reports and none gates.',
|
|
495
|
-
)
|
|
648
|
+
logInfo(densityGatingLine(gated, source))
|
|
496
649
|
|
|
497
650
|
const carrying = reports
|
|
498
651
|
.filter((report) => report.density.length > 0)
|
|
@@ -528,6 +681,20 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
|
528
681
|
)
|
|
529
682
|
}
|
|
530
683
|
|
|
684
|
+
function densityGatingLine(
|
|
685
|
+
gated: GatedList,
|
|
686
|
+
source: 'canon' | 'config' | 'absent',
|
|
687
|
+
): string {
|
|
688
|
+
if (source === 'absent') {
|
|
689
|
+
return `No ${GATED_CONFIG_REL} lists a gated prefix, so every density finding reports and none gates. Run 'canon markdown gated add <prefix>' once a folder reaches zero.`
|
|
690
|
+
}
|
|
691
|
+
const listed =
|
|
692
|
+
gated.prefixes.length === 0 ? 'none yet' : gated.prefixes.join(', ')
|
|
693
|
+
return source === 'canon'
|
|
694
|
+
? `A finding gates under a gated prefix (${listed}), and a bullet run gates in context, docs, and skills already. Every other finding reports.`
|
|
695
|
+
: `A finding gates under a prefix ${GATED_CONFIG_REL} lists (${listed}). Every other finding reports.`
|
|
696
|
+
}
|
|
697
|
+
|
|
531
698
|
const REFERENCE_SHAPE_LABELS: Record<ReferenceShapeKind, string> = {
|
|
532
699
|
'extra-section': 'section outside the shape',
|
|
533
700
|
'section-order': 'section out of order or repeated',
|
|
@@ -546,16 +713,16 @@ function describeReferenceShape(finding: GatedReferenceShapeFinding): string {
|
|
|
546
713
|
*/
|
|
547
714
|
function reportReferenceShape(
|
|
548
715
|
reports: readonly FileReport[],
|
|
549
|
-
|
|
716
|
+
gated: GatedList,
|
|
550
717
|
): void {
|
|
551
718
|
logStep('Reference shape')
|
|
552
719
|
logInfo(
|
|
553
720
|
`A page whose frontmatter carries kind: reference holds its opening, then ${REFERENCE_SECTIONS.join(', ')}, each at most once and in that order, with no H3 and no table of flags. An unmarked page is never measured.`,
|
|
554
721
|
)
|
|
555
722
|
logInfo(
|
|
556
|
-
|
|
723
|
+
gated.prefixes.length > 0
|
|
557
724
|
? 'A finding gates under the prefixes the density step names. Every other finding reports.'
|
|
558
|
-
: '
|
|
725
|
+
: 'No prefix is gated, so every reference shape finding reports and none gates.',
|
|
559
726
|
)
|
|
560
727
|
|
|
561
728
|
const carrying = reports.filter((report) => report.referenceShape.length > 0)
|
package/src/commands/sessions.ts
CHANGED
|
@@ -138,6 +138,14 @@ export function register(program: Command): void {
|
|
|
138
138
|
'cannot rule out a pid handed to an unrelated process, so a roster',
|
|
139
139
|
'reported that way is a candidate list rather than an identity.',
|
|
140
140
|
'',
|
|
141
|
+
'A reader in another pid namespace, such as a sandboxed shell, cannot',
|
|
142
|
+
'see the pid of a record stamped elsewhere. That row is judged on its',
|
|
143
|
+
'messaging socket instead: it stays on the roster as "unverified" with',
|
|
144
|
+
'"via": "socket", since a crashed session leaves its socket behind. A',
|
|
145
|
+
'foreign record whose socket is gone is left off the roster and',
|
|
146
|
+
'counted in "foreignDead", so an empty roster beside a count above zero',
|
|
147
|
+
'is one this reader could not check, not one whose sessions ended.',
|
|
148
|
+
'',
|
|
141
149
|
'The JSON record also carries "statusUpdatedAt" (the stamp the client',
|
|
142
150
|
'wrote beside "status", or null where its record carries none) and',
|
|
143
151
|
'"statusDwellMs" (the elapsed milliseconds since that stamp, falling',
|
|
@@ -464,7 +472,13 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
464
472
|
|
|
465
473
|
intro('canon sessions list')
|
|
466
474
|
reportConfidence(report)
|
|
467
|
-
reportSessions(
|
|
475
|
+
reportSessions(
|
|
476
|
+
shown,
|
|
477
|
+
opts.branch,
|
|
478
|
+
repository,
|
|
479
|
+
own !== null,
|
|
480
|
+
report.foreignDead,
|
|
481
|
+
)
|
|
468
482
|
if (claim) reportClaim(claim)
|
|
469
483
|
outro()
|
|
470
484
|
|
|
@@ -473,6 +487,7 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
473
487
|
`${JSON.stringify({
|
|
474
488
|
dir: report.dir,
|
|
475
489
|
confidence: report.confidence,
|
|
490
|
+
foreignDead: report.foreignDead,
|
|
476
491
|
branch: opts.branch ?? null,
|
|
477
492
|
repository,
|
|
478
493
|
worktree: claim?.worktree ?? null,
|
|
@@ -522,7 +537,9 @@ function reportConfidence(
|
|
|
522
537
|
// per session ever run, which is what makes the empty result worth stating.
|
|
523
538
|
if (report.sessions.length === 0) {
|
|
524
539
|
logInfo(
|
|
525
|
-
|
|
540
|
+
report.foreignDead > 0
|
|
541
|
+
? 'No row to decide.'
|
|
542
|
+
: 'No row to decide. Every record belongs to a session that has ended.',
|
|
526
543
|
)
|
|
527
544
|
return
|
|
528
545
|
}
|
|
@@ -534,6 +551,13 @@ function reportConfidence(
|
|
|
534
551
|
return
|
|
535
552
|
}
|
|
536
553
|
|
|
554
|
+
if (report.sessions.some((session) => session.via === 'socket')) {
|
|
555
|
+
logWarn(
|
|
556
|
+
'This reader sits in another pid namespace than some records, so those rows rest on a messaging socket existing alone. A crashed session leaves its socket behind, so treat the mapping as inferred and open by asking to be corrected.',
|
|
557
|
+
)
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
537
561
|
logWarn(
|
|
538
562
|
'Start times could not be read, so rows rest on a pid answering alone. Treat the mapping as inferred and open by asking to be corrected.',
|
|
539
563
|
)
|
|
@@ -544,6 +568,7 @@ function reportSessions(
|
|
|
544
568
|
branch: string | undefined,
|
|
545
569
|
repository: string | null,
|
|
546
570
|
scoped: boolean,
|
|
571
|
+
foreignDead: number,
|
|
547
572
|
): void {
|
|
548
573
|
logStep('Sessions')
|
|
549
574
|
|
|
@@ -567,7 +592,9 @@ function reportSessions(
|
|
|
567
592
|
}
|
|
568
593
|
|
|
569
594
|
logInfo(
|
|
570
|
-
|
|
595
|
+
foreignDead > 0
|
|
596
|
+
? `No live session could be confirmed. ${foreignDead} of the records come from another pid namespace and no socket answers for them, so this reader could not check whether they ended.`
|
|
597
|
+
: 'No live session. Every record in the registry belongs to a session that has ended.',
|
|
571
598
|
)
|
|
572
599
|
return
|
|
573
600
|
}
|
|
@@ -630,6 +657,12 @@ function reportClaim(claim: ClaimReport): void {
|
|
|
630
657
|
)
|
|
631
658
|
}
|
|
632
659
|
|
|
660
|
+
if (claim.foreignDead > 0) {
|
|
661
|
+
logWarn(
|
|
662
|
+
`${claim.foreignDead} records from another pid namespace have no answering socket, so a session holding this branch may be missing. Treat it as unverified rather than clear.`,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
633
666
|
if (!claim.refsReadable) {
|
|
634
667
|
logWarn(
|
|
635
668
|
'The ref read failed, so this cannot say whether the branch already exists. Treat it as unverified rather than clear.',
|
package/src/commands/tasks.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
claimLabel,
|
|
34
34
|
nextLabel,
|
|
35
35
|
} from '@/tasks/label'
|
|
36
|
+
import { type ClosureOutcome, readClosure } from '@/tasks/closes'
|
|
36
37
|
import { listTasks } from '@/tasks/list'
|
|
37
38
|
import {
|
|
38
39
|
type Claim,
|
|
@@ -142,6 +143,13 @@ interface OutcomeCommandOptions {
|
|
|
142
143
|
readonly root?: string
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
interface ClosesCommandOptions {
|
|
147
|
+
readonly branch?: string
|
|
148
|
+
readonly json?: boolean
|
|
149
|
+
readonly plan?: string
|
|
150
|
+
readonly root?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
interface ListCommandOptions {
|
|
146
154
|
readonly json?: boolean
|
|
147
155
|
readonly root?: string
|
|
@@ -635,6 +643,57 @@ export function register(program: Command): void {
|
|
|
635
643
|
process.exitCode = await runOutcome(task, opts)
|
|
636
644
|
})
|
|
637
645
|
|
|
646
|
+
tasks
|
|
647
|
+
.command('closes')
|
|
648
|
+
.description('Name the issues the branch pull request closes')
|
|
649
|
+
.argument('[task]', 'Task filename stem, as in v28.1-trigger-escalation')
|
|
650
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
651
|
+
.option('--plan <slug>', 'Select the task whose Plan line names this plan')
|
|
652
|
+
.option(
|
|
653
|
+
'--branch <name>',
|
|
654
|
+
'Branch to treat as this pull request, defaulting to the checked-out one',
|
|
655
|
+
)
|
|
656
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
657
|
+
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
658
|
+
.addHelpText(
|
|
659
|
+
'after',
|
|
660
|
+
[
|
|
661
|
+
'',
|
|
662
|
+
'Refusals, read off `reason`:',
|
|
663
|
+
' no-board the repository holds no `.canon/tasks/` board',
|
|
664
|
+
' no-match no task matches the selector',
|
|
665
|
+
' ambiguous more than one task matches the selector',
|
|
666
|
+
' bad-input a task and a plan both named, or neither',
|
|
667
|
+
'',
|
|
668
|
+
'Exit codes:',
|
|
669
|
+
' 0 the task is read, whether or not it closes',
|
|
670
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
671
|
+
'',
|
|
672
|
+
'Reads the `Issue:` lines, each `#NNN` or a comma list, and reports',
|
|
673
|
+
'`issues` and `closing`. The pull request closes them only when it is',
|
|
674
|
+
"the task's last slice, the condition the merge-time archive applies:",
|
|
675
|
+
'no open outcome and no other branch on `Pending branch:`. The branch',
|
|
676
|
+
'and the names it carried before a rename count as this one. Read off',
|
|
677
|
+
'`reason` when `closing` is false:',
|
|
678
|
+
' no-issues the task carries no `Issue:` number',
|
|
679
|
+
' earlier-slice this branch has a pull request on the task that is not',
|
|
680
|
+
' the last listed, so a later number closes the issue',
|
|
681
|
+
' lookup-failed the pull request lookup failed while the task already',
|
|
682
|
+
' lists a pull request, so an earlier slice cannot be ruled out',
|
|
683
|
+
' open-outcomes an outcome is still open, so a later slice closes it',
|
|
684
|
+
' pending-branch another branch is on `Pending branch:`',
|
|
685
|
+
' no-outcomes the task carries no outcomes',
|
|
686
|
+
'',
|
|
687
|
+
'Examples:',
|
|
688
|
+
' canon tasks closes --plan closing-issue-references --json',
|
|
689
|
+
' canon tasks closes v28.1-trigger-escalation --branch feat/x',
|
|
690
|
+
'',
|
|
691
|
+
].join('\n'),
|
|
692
|
+
)
|
|
693
|
+
.action(async (task: string | undefined, opts: ClosesCommandOptions) => {
|
|
694
|
+
process.exitCode = await runCloses(task, opts)
|
|
695
|
+
})
|
|
696
|
+
|
|
638
697
|
tasks
|
|
639
698
|
.command('next-label')
|
|
640
699
|
.description(
|
|
@@ -1196,6 +1255,67 @@ async function branchPullNumbers(
|
|
|
1196
1255
|
}
|
|
1197
1256
|
}
|
|
1198
1257
|
|
|
1258
|
+
/**
|
|
1259
|
+
* The pull request numbers whose head is `branch`, read over REST because a
|
|
1260
|
+
* cloud session's GitHub proxy refuses the GraphQL `gh pr list` runs on.
|
|
1261
|
+
* Unlike `branchPullNumbers`, a failed or unreadable answer is null rather
|
|
1262
|
+
* than an empty list, since the closing decision must not read a failure as
|
|
1263
|
+
* a branch with no pull request.
|
|
1264
|
+
*/
|
|
1265
|
+
async function branchPullNumbersRest(
|
|
1266
|
+
branch: string | undefined,
|
|
1267
|
+
): Promise<readonly number[] | null> {
|
|
1268
|
+
if (branch === undefined) return []
|
|
1269
|
+
|
|
1270
|
+
const owner = await execa(
|
|
1271
|
+
'gh',
|
|
1272
|
+
['api', 'repos/{owner}/{repo}', '--jq', '.owner.login'],
|
|
1273
|
+
{
|
|
1274
|
+
cwd: process.cwd(),
|
|
1275
|
+
reject: false,
|
|
1276
|
+
timeout: GH_TIMEOUT_MS,
|
|
1277
|
+
env: gitEnv(),
|
|
1278
|
+
extendEnv: false,
|
|
1279
|
+
},
|
|
1280
|
+
)
|
|
1281
|
+
if (owner.exitCode !== 0 || owner.stdout.trim() === '') return null
|
|
1282
|
+
|
|
1283
|
+
const result = await execa(
|
|
1284
|
+
'gh',
|
|
1285
|
+
[
|
|
1286
|
+
'api',
|
|
1287
|
+
'-X',
|
|
1288
|
+
'GET',
|
|
1289
|
+
'repos/{owner}/{repo}/pulls',
|
|
1290
|
+
'-f',
|
|
1291
|
+
`head=${owner.stdout.trim()}:${branch}`,
|
|
1292
|
+
'-f',
|
|
1293
|
+
'state=all',
|
|
1294
|
+
'--jq',
|
|
1295
|
+
'[.[].number]',
|
|
1296
|
+
],
|
|
1297
|
+
{
|
|
1298
|
+
cwd: process.cwd(),
|
|
1299
|
+
reject: false,
|
|
1300
|
+
timeout: GH_TIMEOUT_MS,
|
|
1301
|
+
env: gitEnv(),
|
|
1302
|
+
extendEnv: false,
|
|
1303
|
+
},
|
|
1304
|
+
)
|
|
1305
|
+
if (result.exitCode !== 0) return null
|
|
1306
|
+
|
|
1307
|
+
try {
|
|
1308
|
+
const numbers: unknown = JSON.parse(result.stdout)
|
|
1309
|
+
if (!Array.isArray(numbers)) return null
|
|
1310
|
+
|
|
1311
|
+
return numbers.every((number) => Number.isInteger(number))
|
|
1312
|
+
? (numbers as number[])
|
|
1313
|
+
: null
|
|
1314
|
+
} catch {
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1199
1319
|
async function runPlanLink(
|
|
1200
1320
|
task: string,
|
|
1201
1321
|
plan: string,
|
|
@@ -1381,6 +1501,66 @@ function reportPlanLink(
|
|
|
1381
1501
|
return 0
|
|
1382
1502
|
}
|
|
1383
1503
|
|
|
1504
|
+
async function runCloses(
|
|
1505
|
+
task: string | undefined,
|
|
1506
|
+
opts: ClosesCommandOptions,
|
|
1507
|
+
): Promise<number> {
|
|
1508
|
+
const emitJson = opts.json ?? false
|
|
1509
|
+
const selector = selectorFor(task, opts.plan)
|
|
1510
|
+
|
|
1511
|
+
if ('ok' in selector) {
|
|
1512
|
+
return reportRecord('canon tasks closes', selector, emitJson, process.cwd())
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
1516
|
+
const branch = opts.branch ?? (await workingBranch())
|
|
1517
|
+
const branches = branch ? [branch, ...(await formerNames(branch))] : []
|
|
1518
|
+
const outcome = await readClosure(
|
|
1519
|
+
root,
|
|
1520
|
+
selector,
|
|
1521
|
+
branches,
|
|
1522
|
+
await branchPullNumbersRest(branch),
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
return reportCloses(outcome, emitJson, root)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function reportCloses(
|
|
1529
|
+
outcome: ClosureOutcome,
|
|
1530
|
+
emitJson: boolean,
|
|
1531
|
+
root: string,
|
|
1532
|
+
): number {
|
|
1533
|
+
if (!outcome.ok) {
|
|
1534
|
+
return reportRecord('canon tasks closes', outcome, emitJson, root)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (emitJson) {
|
|
1538
|
+
process.stdout.write(
|
|
1539
|
+
`${JSON.stringify({
|
|
1540
|
+
ok: true,
|
|
1541
|
+
root,
|
|
1542
|
+
task: outcome.stem,
|
|
1543
|
+
path: relative(root, outcome.path),
|
|
1544
|
+
issues: outcome.issues,
|
|
1545
|
+
closing: outcome.closing,
|
|
1546
|
+
reason: outcome.reason,
|
|
1547
|
+
})}\n`,
|
|
1548
|
+
)
|
|
1549
|
+
return 0
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
intro('canon tasks closes')
|
|
1553
|
+
logStep(outcome.closing ? 'Closes' : 'Closes nothing')
|
|
1554
|
+
logInfo(
|
|
1555
|
+
outcome.issues.length > 0
|
|
1556
|
+
? `${outcome.stem} names ${outcome.issues.map((n) => `#${n}`).join(', ')}, ${outcome.reason}`
|
|
1557
|
+
: `${outcome.stem} names no issue`,
|
|
1558
|
+
)
|
|
1559
|
+
outro()
|
|
1560
|
+
|
|
1561
|
+
return 0
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1384
1564
|
function reportOutcome(
|
|
1385
1565
|
outcome: CloseOutcome,
|
|
1386
1566
|
emitJson: boolean,
|
|
@@ -109,6 +109,8 @@ const UNREADABLE: Record<Unreadable, string> = {
|
|
|
109
109
|
'The pull request read failed, so every worktree would report as unmerged whatever its real state. Authenticate gh and run this again.',
|
|
110
110
|
'sessions-unreadable':
|
|
111
111
|
'The session roster could not be read, so nothing was read about which worktrees are still held.',
|
|
112
|
+
'sessions-unchecked':
|
|
113
|
+
'Some session records come from another pid namespace and no socket answers for them, so a live holder may be missing from the roster. Run this from the namespace the sessions run in.',
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
const CONDITIONS = [
|