@erclx/canon 5.8.0 → 5.10.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 +51 -0
- package/claude/skills/api-design/SKILL.md +102 -0
- package/claude/skills/api-design/references/adopted.md +40 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/review-craft/REQUIREMENT.md +8 -2
- package/claude/skills/review-craft/SKILL.md +63 -7
- package/claude/skills/review-craft/references/adopted.md +49 -0
- package/claude/skills/review-ui/REQUIREMENT.md +3 -0
- package/claude/skills/review-ui/SKILL.md +15 -1
- package/claude/skills/review-ui/references/driving.md +3 -3
- package/claude/skills/review-ui/references/post.md +16 -3
- package/claude/skills/systematic-debugging/REQUIREMENT.md +14 -0
- package/claude/skills/systematic-debugging/SKILL.md +45 -17
- package/claude/skills/systematic-debugging/references/adopted.md +44 -0
- package/docs/agents/capture.md +2 -2
- package/docs/agents/commands.md +3 -1
- package/docs/agents/hooks.md +56 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +33 -17
- package/docs/agents/records-push.md +1 -1
- package/docs/agents/records-validate.md +1 -1
- package/docs/agents/scripting.md +6 -3
- package/docs/agents/tasks-archive.md +4 -2
- package/docs/agents/tasks-plans.md +8 -5
- package/docs/agents/tasks.md +1 -1
- package/docs/agents/worktrees.md +2 -2
- package/docs/target/scenarios.md +2 -2
- package/docs/workflow/skill-map.md +1 -0
- package/package.json +1 -1
- package/scripts/core/regen-agent-fixture.sh +3 -3
- package/scripts/core/regen-hero.ts +119 -0
- package/scripts/core/regen-web-favicon.ts +1 -1
- package/src/capture/frame-values.ts +359 -0
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +3 -0
- package/src/cli.ts +2 -0
- package/src/commands/claude.ts +1 -1
- package/src/commands/docs.ts +51 -11
- package/src/commands/hooks.ts +67 -0
- package/src/commands/pr.ts +146 -76
- package/src/commands/standards.ts +33 -2
- package/src/commands/tasks.ts +57 -2
- package/src/commands/tooling.ts +5 -5
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/worktrees.ts +4 -3
- package/src/counts/catalogs.ts +3 -3
- package/src/design/tokens.ts +1 -1
- package/src/docs/list.ts +103 -0
- package/src/docs/read.ts +63 -19
- package/src/gate/stages.ts +1 -1
- package/src/gov/stacks.ts +1 -1
- package/src/help.ts +2 -1
- package/src/hooks/post-merge.ts +331 -0
- package/src/pr/evidence.ts +21 -0
- package/src/records/validate.ts +1 -38
- package/src/standards/list.ts +33 -0
- package/src/standards/read.ts +13 -0
- package/src/standards/scope.ts +56 -0
- package/src/sync/check.ts +38 -4
- package/src/tasks/archive.ts +195 -3
- package/src/tasks/record.ts +25 -2
- package/src/tooling/inject.ts +4 -3
- package/src/tooling/manifest.ts +62 -8
- package/src/tooling/scan.ts +4 -3
- package/src/tooling/stamp.ts +11 -2
- package/src/tooling/subfolder.ts +7 -3
- package/standards/tasks.md +2 -2
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/scripts/verify.json +3 -0
- package/tooling/base/configs/scripts/verify.sh +1 -110
- package/tooling/base/configs/scripts/verify.ts +378 -0
- package/tooling/base/manifest.toml +1 -0
- package/tooling/base/reference.md +5 -3
- package/tooling/go/configs/scripts/verify.json +10 -0
- package/tooling/go/reference.md +5 -4
- package/tooling/php/configs/scripts/verify.json +7 -0
- package/tooling/php/reference.md +5 -4
- package/tooling/python/configs/scripts/verify.json +4 -0
- package/tooling/python/reference.md +3 -3
- package/tooling/web/configs/scripts/verify.json +3 -0
- package/tooling/web/reference.md +3 -3
- package/scripts/core/regen-hero.sh +0 -419
- package/scripts/docs/list.sh +0 -184
- package/scripts/lib/frontmatter.sh +0 -30
- package/scripts/standards/list.sh +0 -115
- package/scripts/tooling/create.sh +0 -109
- package/src/commands/pass-through.ts +0 -39
- package/tooling/go/configs/scripts/verify.sh +0 -142
- package/tooling/php/configs/scripts/verify.sh +0 -147
- package/tooling/python/configs/scripts/verify.sh +0 -141
- package/tooling/web/configs/scripts/verify.sh +0 -153
package/src/commands/pr.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
withMergeState,
|
|
27
27
|
} from '@/pr/checks'
|
|
28
28
|
import {
|
|
29
|
+
findEvidenceCaseCount,
|
|
29
30
|
findEvidenceChecklist,
|
|
30
31
|
findEvidenceCommentId,
|
|
31
32
|
findEvidenceLocal,
|
|
@@ -173,20 +174,21 @@ const PULL_REFUSALS: Record<PullRefusal | HeadRefusal, string> = {
|
|
|
173
174
|
type EvidenceRefusal =
|
|
174
175
|
| 'gh-failed'
|
|
175
176
|
| 'no-base'
|
|
176
|
-
| 'unreadable-tree'
|
|
177
177
|
| 'unreadable-changes'
|
|
178
178
|
| 'unreadable-checklist'
|
|
179
|
+
| 'would-empty'
|
|
179
180
|
|
|
180
181
|
const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
181
182
|
'gh-failed':
|
|
182
183
|
'gh could not answer for this repository. Name the pull request number.',
|
|
183
184
|
'unreadable-checklist':
|
|
184
185
|
'The file named by --checklist could not be read, or holds nothing. Rendering without it would drop the only copy, since the caller deletes the handoff once a post reports success.',
|
|
185
|
-
'no-base':
|
|
186
|
-
|
|
187
|
-
'git could not read the tree at the base commit, so no path could be judged added or changed.',
|
|
186
|
+
'no-base':
|
|
187
|
+
'GitHub reported no merge base for this pull request, so no path could be judged added or changed.',
|
|
188
188
|
'unreadable-changes':
|
|
189
|
-
'
|
|
189
|
+
'GitHub could not list what this pull request changed, so the set is unknown.',
|
|
190
|
+
'would-empty':
|
|
191
|
+
'This render holds no cases and the marked comment holds some, so posting it would replace a comparison with an empty one. Edit the comment by hand if every case was removed on purpose.',
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/** Why the read produced no comparison, ahead of the ones the compare owns. */
|
|
@@ -427,11 +429,13 @@ export function register(program: Command): void {
|
|
|
427
429
|
'after',
|
|
428
430
|
[
|
|
429
431
|
'',
|
|
430
|
-
|
|
431
|
-
'the
|
|
432
|
-
'
|
|
433
|
-
'
|
|
434
|
-
'
|
|
432
|
+
"Compares the pull request's merge base against its head, both read over",
|
|
433
|
+
'the API for the number named, so the record is the same from any',
|
|
434
|
+
'checkout. It never compares the previous push against the new one, so',
|
|
435
|
+
'the comment never claims more than the pull request currently shows.',
|
|
436
|
+
'A path counts as evidence when one of its segments is literally',
|
|
437
|
+
'`evidence` and the filename carries an image extension (png, jpg,',
|
|
438
|
+
'jpeg, gif, webp, avif, svg). A README, a capture',
|
|
435
439
|
'script, or a raw data file kept beside the images is left out rather',
|
|
436
440
|
'than rendered as a broken embed.',
|
|
437
441
|
'',
|
|
@@ -440,6 +444,8 @@ export function register(program: Command): void {
|
|
|
440
444
|
' comment already exists and should be edited in place',
|
|
441
445
|
' no-evidence nothing in the diff carries an evidence/ segment, which',
|
|
442
446
|
' is an ordinary, silent no-op rather than a refusal',
|
|
447
|
+
' would-empty the render holds no cases and the marked comment holds',
|
|
448
|
+
' some, so no body is printed and a person edits the comment',
|
|
443
449
|
'',
|
|
444
450
|
"--preview puts the address on the body's first line. With no evidence",
|
|
445
451
|
'in the diff, the body is that line and the marker alone, reported as',
|
|
@@ -1346,21 +1352,63 @@ async function runReviewState(
|
|
|
1346
1352
|
}
|
|
1347
1353
|
|
|
1348
1354
|
/**
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1355
|
+
* The paths a pull request changed that still exist at its head, each tagged
|
|
1356
|
+
* with whether the pull request added it, read through the paginated files
|
|
1357
|
+
* endpoint so the set is the same from any checkout and never capped at the
|
|
1358
|
+
* first view. `renamed` and `copied` count as added, since the new path has no
|
|
1359
|
+
* counterpart at the merge base. Returns undefined when the read fails, which
|
|
1360
|
+
* refuses rather than rendering a short set.
|
|
1353
1361
|
*/
|
|
1354
|
-
async function
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
): Promise<
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1362
|
+
async function listPullFiles(
|
|
1363
|
+
cwd: string,
|
|
1364
|
+
number: number,
|
|
1365
|
+
): Promise<ReadonlyMap<string, boolean> | undefined> {
|
|
1366
|
+
const stdout = await gh(cwd, [
|
|
1367
|
+
'api',
|
|
1368
|
+
'--paginate',
|
|
1369
|
+
`repos/{owner}/{repo}/pulls/${number}/files`,
|
|
1370
|
+
'--jq',
|
|
1371
|
+
'.[] | [.status, .filename] | @tsv',
|
|
1372
|
+
])
|
|
1373
|
+
if (stdout === null) return undefined
|
|
1374
|
+
const files = new Map<string, boolean>()
|
|
1375
|
+
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
1376
|
+
const [status, path] = line.split('\t')
|
|
1377
|
+
if (status === undefined || path === undefined || status === 'removed') {
|
|
1378
|
+
continue
|
|
1379
|
+
}
|
|
1380
|
+
files.set(path, status !== 'modified' && status !== 'changed')
|
|
1381
|
+
}
|
|
1382
|
+
return files
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* The commit the pull request diverged from its base at, from the compare
|
|
1387
|
+
* endpoint, since a checkout on another branch resolves its own trunk tip
|
|
1388
|
+
* instead and the base image links would then describe the wrong commit.
|
|
1389
|
+
*/
|
|
1390
|
+
async function readPullMergeBase(
|
|
1391
|
+
cwd: string,
|
|
1392
|
+
number: number,
|
|
1393
|
+
head: string,
|
|
1394
|
+
): Promise<string | undefined> {
|
|
1395
|
+
const baseRef = await gh(cwd, [
|
|
1396
|
+
'pr',
|
|
1397
|
+
'view',
|
|
1398
|
+
String(number),
|
|
1399
|
+
'--json',
|
|
1400
|
+
'baseRefName',
|
|
1401
|
+
'--jq',
|
|
1402
|
+
'.baseRefName',
|
|
1403
|
+
])
|
|
1404
|
+
if (baseRef === null || baseRef.trim() === '') return undefined
|
|
1405
|
+
const sha = await gh(cwd, [
|
|
1406
|
+
'api',
|
|
1407
|
+
`repos/{owner}/{repo}/compare/${baseRef.trim()}...${head}`,
|
|
1408
|
+
'--jq',
|
|
1409
|
+
'.merge_base_commit.sha',
|
|
1410
|
+
])
|
|
1411
|
+
return sha === null || sha.trim() === '' ? undefined : sha.trim()
|
|
1364
1412
|
}
|
|
1365
1413
|
|
|
1366
1414
|
async function runEvidence(
|
|
@@ -1404,16 +1452,20 @@ async function runEvidence(
|
|
|
1404
1452
|
)
|
|
1405
1453
|
}
|
|
1406
1454
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1455
|
+
if (identity.number === undefined) {
|
|
1456
|
+
return refuseWith(
|
|
1457
|
+
'gh-failed',
|
|
1458
|
+
EVIDENCE_REFUSALS['gh-failed'],
|
|
1459
|
+
emitJson,
|
|
1460
|
+
root,
|
|
1461
|
+
)
|
|
1410
1462
|
}
|
|
1411
1463
|
|
|
1412
|
-
const [
|
|
1413
|
-
|
|
1414
|
-
|
|
1464
|
+
const [pullFiles, base] = await Promise.all([
|
|
1465
|
+
listPullFiles(root, identity.number),
|
|
1466
|
+
readPullMergeBase(root, identity.number, identity.head),
|
|
1415
1467
|
])
|
|
1416
|
-
if (
|
|
1468
|
+
if (pullFiles === undefined) {
|
|
1417
1469
|
return refuseWith(
|
|
1418
1470
|
'unreadable-changes',
|
|
1419
1471
|
EVIDENCE_REFUSALS['unreadable-changes'],
|
|
@@ -1421,61 +1473,57 @@ async function runEvidence(
|
|
|
1421
1473
|
root,
|
|
1422
1474
|
)
|
|
1423
1475
|
}
|
|
1424
|
-
if (
|
|
1425
|
-
return refuseWith(
|
|
1426
|
-
'unreadable-tree',
|
|
1427
|
-
EVIDENCE_REFUSALS['unreadable-tree'],
|
|
1428
|
-
emitJson,
|
|
1429
|
-
root,
|
|
1430
|
-
)
|
|
1476
|
+
if (base === undefined) {
|
|
1477
|
+
return refuseWith('no-base', EVIDENCE_REFUSALS['no-base'], emitJson, root)
|
|
1431
1478
|
}
|
|
1432
1479
|
|
|
1433
|
-
const grouped = await groupEvidence(
|
|
1434
|
-
|
|
1480
|
+
const grouped = await groupEvidence(
|
|
1481
|
+
[...pullFiles.keys()],
|
|
1482
|
+
async (path) => pullFiles.get(path) === false,
|
|
1435
1483
|
)
|
|
1436
1484
|
|
|
1485
|
+
let existingCases = 0
|
|
1437
1486
|
let commentId: number | undefined
|
|
1438
1487
|
let carriedPreview: string | undefined
|
|
1439
1488
|
let carriedChecklist: string | undefined
|
|
1440
1489
|
let carriedLocal: string | undefined
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
comments = undefined
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
if (comments === undefined) {
|
|
1467
|
-
return refuseWith(
|
|
1468
|
-
'gh-failed',
|
|
1469
|
-
EVIDENCE_REFUSALS['gh-failed'],
|
|
1470
|
-
emitJson,
|
|
1471
|
-
root,
|
|
1472
|
-
)
|
|
1490
|
+
const commentsRow = await gh(root, [
|
|
1491
|
+
'pr',
|
|
1492
|
+
'view',
|
|
1493
|
+
String(identity.number),
|
|
1494
|
+
'--json',
|
|
1495
|
+
'comments',
|
|
1496
|
+
])
|
|
1497
|
+
// An unread thread refuses rather than rendering, since a body built
|
|
1498
|
+
// without it knows neither the comment to edit nor the preview address
|
|
1499
|
+
// to carry, and posting it would duplicate the comment and drop the link.
|
|
1500
|
+
// It refuses ahead of no-evidence too, so that record never reports an
|
|
1501
|
+
// absent field it did not read.
|
|
1502
|
+
let comments: readonly { url?: string; body: string }[] | undefined
|
|
1503
|
+
if (commentsRow !== null) {
|
|
1504
|
+
try {
|
|
1505
|
+
comments = (
|
|
1506
|
+
JSON.parse(commentsRow) as {
|
|
1507
|
+
comments?: readonly { url?: string; body: string }[]
|
|
1508
|
+
}
|
|
1509
|
+
).comments
|
|
1510
|
+
} catch {
|
|
1511
|
+
comments = undefined
|
|
1473
1512
|
}
|
|
1474
|
-
commentId = findEvidenceCommentId(comments)
|
|
1475
|
-
carriedPreview = findEvidencePreview(comments)
|
|
1476
|
-
carriedChecklist = findEvidenceChecklist(comments)
|
|
1477
|
-
carriedLocal = findEvidenceLocal(comments)
|
|
1478
1513
|
}
|
|
1514
|
+
if (comments === undefined) {
|
|
1515
|
+
return refuseWith(
|
|
1516
|
+
'gh-failed',
|
|
1517
|
+
EVIDENCE_REFUSALS['gh-failed'],
|
|
1518
|
+
emitJson,
|
|
1519
|
+
root,
|
|
1520
|
+
)
|
|
1521
|
+
}
|
|
1522
|
+
commentId = findEvidenceCommentId(comments)
|
|
1523
|
+
existingCases = findEvidenceCaseCount(comments)
|
|
1524
|
+
carriedPreview = findEvidencePreview(comments)
|
|
1525
|
+
carriedChecklist = findEvidenceChecklist(comments)
|
|
1526
|
+
carriedLocal = findEvidenceLocal(comments)
|
|
1479
1527
|
|
|
1480
1528
|
// What the marked comment already shows a reviewer, kept apart from the
|
|
1481
1529
|
// flags this call passed so a reader learns what is posted.
|
|
@@ -1543,6 +1591,28 @@ async function runEvidence(
|
|
|
1543
1591
|
|
|
1544
1592
|
const caseCount = states.reduce((n, s) => n + s.items.length, 0)
|
|
1545
1593
|
|
|
1594
|
+
// A render with no cases cannot tell a removal from a short read, so it
|
|
1595
|
+
// never replaces a comment that carries some. The record keeps the comment
|
|
1596
|
+
// id and the carried fields so the caller can still edit by hand.
|
|
1597
|
+
if (caseCount === 0 && existingCases > 0) {
|
|
1598
|
+
logStep('Refused')
|
|
1599
|
+
logWarn(EVIDENCE_REFUSALS['would-empty'])
|
|
1600
|
+
outro()
|
|
1601
|
+
if (emitJson) {
|
|
1602
|
+
process.stdout.write(
|
|
1603
|
+
`${JSON.stringify({
|
|
1604
|
+
root,
|
|
1605
|
+
number: identity.number,
|
|
1606
|
+
reason: 'would-empty',
|
|
1607
|
+
message: EVIDENCE_REFUSALS['would-empty'],
|
|
1608
|
+
...(commentId !== undefined && { commentId }),
|
|
1609
|
+
...carried,
|
|
1610
|
+
})}\n`,
|
|
1611
|
+
)
|
|
1612
|
+
}
|
|
1613
|
+
return 1
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1546
1616
|
logStep('Scope')
|
|
1547
1617
|
logInfo(
|
|
1548
1618
|
`${plural(caseCount, 'case')} across ${plural(states.length, 'state')}`,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
|
-
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
4
3
|
import {
|
|
5
4
|
auditExitCode,
|
|
6
5
|
auditStandards,
|
|
7
6
|
type StandardsAudit,
|
|
8
7
|
} from '@/standards/audit'
|
|
8
|
+
import { listStandardEntries } from '@/standards/list'
|
|
9
9
|
import { listStandards, readStandard, resolveStandard } from '@/standards/read'
|
|
10
10
|
import {
|
|
11
11
|
frameError,
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
plural,
|
|
20
20
|
} from '@/ui'
|
|
21
21
|
|
|
22
|
+
interface StandardsListOptions {
|
|
23
|
+
readonly json?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
interface StandardsAuditOptions {
|
|
23
27
|
readonly json?: boolean
|
|
24
28
|
readonly arrivalsOnly?: boolean
|
|
@@ -57,7 +61,14 @@ export function register(program: Command): void {
|
|
|
57
61
|
process.exitCode = print(name)
|
|
58
62
|
})
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
standards
|
|
65
|
+
.command('list')
|
|
66
|
+
.description('List the standards with their descriptions and jurisdiction')
|
|
67
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
68
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
69
|
+
.action((opts: StandardsListOptions) => {
|
|
70
|
+
process.exitCode = runStandardsList(opts)
|
|
71
|
+
})
|
|
61
72
|
|
|
62
73
|
standards
|
|
63
74
|
.command('audit')
|
|
@@ -95,6 +106,26 @@ export function register(program: Command): void {
|
|
|
95
106
|
})
|
|
96
107
|
}
|
|
97
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Reads the working root first, the way `canon standards <name>` does, so a
|
|
111
|
+
* project's own standards list beside the package corpus. The `--json` record
|
|
112
|
+
* is the one thing on stdout and the frame opens only on the text path.
|
|
113
|
+
*/
|
|
114
|
+
function runStandardsList(opts: StandardsListOptions): number {
|
|
115
|
+
const standards = listStandardEntries(process.cwd())
|
|
116
|
+
|
|
117
|
+
if (opts.json) {
|
|
118
|
+
process.stdout.write(`${JSON.stringify({ standards })}\n`)
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
intro('canon standards')
|
|
123
|
+
logStep('Standards')
|
|
124
|
+
for (const each of standards) logInfo(`${each.name} : ${each.description}`)
|
|
125
|
+
outro()
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
/**
|
|
99
130
|
* Writes the standard to stdout and every frame line to stderr, so a caller
|
|
100
131
|
* capturing the output with `$(...)` receives the document alone.
|
package/src/commands/tasks.ts
CHANGED
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
type CitationOutcome,
|
|
11
11
|
type DeclineOutcome,
|
|
12
12
|
declineTask,
|
|
13
|
+
type InboundRelink,
|
|
13
14
|
type PlanCitations,
|
|
14
15
|
type PlanMove,
|
|
15
16
|
planCitations,
|
|
17
|
+
type RelinkFailure,
|
|
16
18
|
} from '@/tasks/archive'
|
|
17
19
|
import {
|
|
18
20
|
type LabelOutcome,
|
|
@@ -112,6 +114,7 @@ interface PullRequestCommandOptions {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
interface PlanLinkCommandOptions {
|
|
117
|
+
readonly add?: boolean
|
|
115
118
|
readonly json?: boolean
|
|
116
119
|
readonly root?: string
|
|
117
120
|
}
|
|
@@ -452,6 +455,10 @@ export function register(program: Command): void {
|
|
|
452
455
|
.argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
|
|
453
456
|
.argument('<plan>', 'Plan path or its slug, as in dispatch-answer-gate')
|
|
454
457
|
.helpOption('-h, --help', 'Show this help message')
|
|
458
|
+
.option(
|
|
459
|
+
'--add',
|
|
460
|
+
'Append the plan to the line, for a task sliced over plans',
|
|
461
|
+
)
|
|
455
462
|
.option('--json', 'Emit a machine-readable record on stdout')
|
|
456
463
|
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
457
464
|
.addHelpText(
|
|
@@ -459,7 +466,7 @@ export function register(program: Command): void {
|
|
|
459
466
|
[
|
|
460
467
|
'',
|
|
461
468
|
'Exit codes:',
|
|
462
|
-
' 0 the line was added, corrected, or already correct',
|
|
469
|
+
' 0 the line was added, appended to, corrected, or already correct',
|
|
463
470
|
' 1 refused, including several-plans for a line linking more than one',
|
|
464
471
|
' plan, with the reason on stderr or in the JSON record',
|
|
465
472
|
'',
|
|
@@ -469,8 +476,13 @@ export function register(program: Command): void {
|
|
|
469
476
|
'tasks plan-answers does. Safe from a linked worktree, since it',
|
|
470
477
|
'resolves the board root in-process.',
|
|
471
478
|
'',
|
|
479
|
+
'With --add it keeps every link in order and appends the plan, so a',
|
|
480
|
+
'sliced task links one plan per slice. It reports unchanged when the',
|
|
481
|
+
'plan is already listed and is allowed on a line holding several.',
|
|
482
|
+
'',
|
|
472
483
|
'Examples:',
|
|
473
484
|
' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
|
|
485
|
+
' canon tasks plan-link v28.1-trigger-escalation second-slice --add',
|
|
474
486
|
' canon tasks plan-link v28.1-trigger-escalation .canon/plans/feature-dispatch-answer-gate.md --json',
|
|
475
487
|
'',
|
|
476
488
|
].join('\n'),
|
|
@@ -723,7 +735,7 @@ async function runPlanLink(
|
|
|
723
735
|
): Promise<number> {
|
|
724
736
|
const emitJson = opts.json ?? false
|
|
725
737
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
726
|
-
const outcome = await recordPlan(root, task, plan)
|
|
738
|
+
const outcome = await recordPlan(root, task, plan, opts.add ?? false)
|
|
727
739
|
|
|
728
740
|
return reportPlanLink(outcome, emitJson, root)
|
|
729
741
|
}
|
|
@@ -1493,6 +1505,7 @@ function report(
|
|
|
1493
1505
|
logInfo('retargeted the Ready: line')
|
|
1494
1506
|
}
|
|
1495
1507
|
if (outcome.priorityRowRemoved) logInfo('cleared the ordering row')
|
|
1508
|
+
logRelinked(outcome.relinked, outcome.relinkFailed, root)
|
|
1496
1509
|
if (outcome.indexRegenerated) logInfo('regenerated index.md')
|
|
1497
1510
|
if (outcome.cut > 0) logInfo(`${outcome.cut} outcome(s) cut`)
|
|
1498
1511
|
outro()
|
|
@@ -1508,6 +1521,43 @@ function logPlanMoves(plans: readonly PlanMove[], root: string): void {
|
|
|
1508
1521
|
if (plans.length > 0) logInfo('retargeted the Plan: line')
|
|
1509
1522
|
}
|
|
1510
1523
|
|
|
1524
|
+
function logRelinked(
|
|
1525
|
+
relinked: readonly InboundRelink[],
|
|
1526
|
+
failed: readonly RelinkFailure[],
|
|
1527
|
+
root: string,
|
|
1528
|
+
): void {
|
|
1529
|
+
if (relinked.length > 0) {
|
|
1530
|
+
const links = relinked.reduce((sum, entry) => sum + entry.links, 0)
|
|
1531
|
+
logInfo(`repointed ${links} link(s) in ${relinked.length} file(s)`)
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
for (const failure of failed) {
|
|
1535
|
+
logWarn(
|
|
1536
|
+
`could not repoint ${relative(root, failure.file)}: ${failure.message}`,
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function relinkFailedFor(
|
|
1542
|
+
failed: readonly RelinkFailure[],
|
|
1543
|
+
root: string,
|
|
1544
|
+
): readonly { readonly file: string; readonly message: string }[] {
|
|
1545
|
+
return failed.map((failure) => ({
|
|
1546
|
+
file: relative(root, failure.file),
|
|
1547
|
+
message: failure.message,
|
|
1548
|
+
}))
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function relinkedFor(
|
|
1552
|
+
relinked: readonly InboundRelink[],
|
|
1553
|
+
root: string,
|
|
1554
|
+
): readonly { readonly file: string; readonly links: number }[] {
|
|
1555
|
+
return relinked.map((entry) => ({
|
|
1556
|
+
file: relative(root, entry.file),
|
|
1557
|
+
links: entry.links,
|
|
1558
|
+
}))
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1511
1561
|
function planMovesFor(
|
|
1512
1562
|
plans: readonly PlanMove[],
|
|
1513
1563
|
root: string,
|
|
@@ -1547,6 +1597,8 @@ function recordFor(
|
|
|
1547
1597
|
: null,
|
|
1548
1598
|
closed: outcome.closed,
|
|
1549
1599
|
cut: outcome.cut,
|
|
1600
|
+
relinked: relinkedFor(outcome.relinked, root),
|
|
1601
|
+
relinkFailed: relinkFailedFor(outcome.relinkFailed, root),
|
|
1550
1602
|
}
|
|
1551
1603
|
}
|
|
1552
1604
|
|
|
@@ -1639,6 +1691,7 @@ function reportDecline(
|
|
|
1639
1691
|
logPlanMoves(outcome.plans, root)
|
|
1640
1692
|
if (outcome.priorityRowRemoved) logInfo('cleared the ordering row')
|
|
1641
1693
|
if (outcome.backlogRowRemoved) logInfo('cleared the backlog row')
|
|
1694
|
+
logRelinked(outcome.relinked, outcome.relinkFailed, root)
|
|
1642
1695
|
if (outcome.indexRegenerated) logInfo('regenerated index.md')
|
|
1643
1696
|
outro()
|
|
1644
1697
|
|
|
@@ -1667,5 +1720,7 @@ function declineRecordFor(
|
|
|
1667
1720
|
backlogRowRemoved: outcome.backlogRowRemoved,
|
|
1668
1721
|
indexRegenerated: outcome.indexRegenerated,
|
|
1669
1722
|
plans: planMovesFor(outcome.plans, root),
|
|
1723
|
+
relinked: relinkedFor(outcome.relinked, root),
|
|
1724
|
+
relinkFailed: relinkFailedFor(outcome.relinkFailed, root),
|
|
1670
1725
|
}
|
|
1671
1726
|
}
|
package/src/commands/tooling.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from '@/tooling/manifest'
|
|
20
20
|
import { readReference, resolveReference } from '@/tooling/read'
|
|
21
21
|
import { scan, type ScanResult } from '@/tooling/scan'
|
|
22
|
-
import { recordToolingChain } from '@/tooling/stamp'
|
|
22
|
+
import { recordedStacks, recordToolingChain } from '@/tooling/stamp'
|
|
23
23
|
import { subfolderPath } from '@/tooling/subfolder'
|
|
24
24
|
import {
|
|
25
25
|
intro,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
select,
|
|
35
35
|
} from '@/ui'
|
|
36
36
|
|
|
37
|
-
const PASS_THROUGH_VERBS = ['
|
|
37
|
+
const PASS_THROUGH_VERBS = ['verify'] as const
|
|
38
38
|
|
|
39
39
|
interface SyncOptions {
|
|
40
40
|
readonly skip?: string
|
|
@@ -71,7 +71,7 @@ type Prepared =
|
|
|
71
71
|
export function register(program: Command): void {
|
|
72
72
|
const tooling = program
|
|
73
73
|
.command('tooling')
|
|
74
|
-
.description('Manage tooling stacks (sync, reference
|
|
74
|
+
.description('Manage tooling stacks (sync, reference)')
|
|
75
75
|
.helpOption('-h, --help', 'Show this help message')
|
|
76
76
|
|
|
77
77
|
tooling
|
|
@@ -228,7 +228,7 @@ export function register(program: Command): void {
|
|
|
228
228
|
*
|
|
229
229
|
* The frame opens after the `--json` return. The bash emitted a closing `└`
|
|
230
230
|
* from its EXIT trap with no `┌` above it, because the hand-rolled
|
|
231
|
-
* pass-through loop below
|
|
231
|
+
* pass-through loop below opens no frame before it runs the script.
|
|
232
232
|
*/
|
|
233
233
|
function runList(opts: ListOptions): number {
|
|
234
234
|
// The warning is a frame-interior line, so it goes out after `intro` on the
|
|
@@ -485,7 +485,7 @@ async function stampChain(chain: Manifest[], target: string): Promise<void> {
|
|
|
485
485
|
)
|
|
486
486
|
|
|
487
487
|
if (recorded) {
|
|
488
|
-
logInfo(`Recorded chain: ${chain
|
|
488
|
+
logInfo(`Recorded chain: ${recordedStacks(chain).join(' < ')}`)
|
|
489
489
|
return
|
|
490
490
|
}
|
|
491
491
|
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -37,7 +37,7 @@ interface UpgradeRecord {
|
|
|
37
37
|
readonly reason?: string
|
|
38
38
|
/**
|
|
39
39
|
* One rendered line for a caller that reports the outcome without parsing
|
|
40
|
-
* the rest of the record, such as
|
|
40
|
+
* the rest of the record, such as the base stack's post-merge hook. `current` reuses
|
|
41
41
|
* `describeSkew` verbatim so its wording never drifts from the line `canon
|
|
42
42
|
* sync --check` and `canon claude skills drift` already report.
|
|
43
43
|
*/
|
|
@@ -312,7 +312,8 @@ function emit(opts: UpgradeOptions, record: UpgradeRecord): void {
|
|
|
312
312
|
/**
|
|
313
313
|
* `message` is the one field carrying arbitrary text: `describeSkew`'s
|
|
314
314
|
* `unknown` branch embeds a registry error verbatim, and a registry answering
|
|
315
|
-
* with HTML produces one already carrying a double quote.
|
|
315
|
+
* with HTML produces one already carrying a double quote. The base stack's
|
|
316
|
+
* post-merge hook, and any hook installed before `canon hooks post-merge`,
|
|
316
317
|
* reads this field with a pattern rather than a parser, so a quote makes it
|
|
317
318
|
* through `JSON.stringify` as an escaped `\"` that the pattern stops at,
|
|
318
319
|
* truncating the line it prints. Collapsing whitespace and swapping the quote
|
|
@@ -35,10 +35,11 @@ interface ReclaimCommandOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* What a caller reads instead of the frame.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* The post-merge hooks are the callers that are not a person, and they branch
|
|
39
39
|
* on a parsed field rather than on the exit code, since a shell profile
|
|
40
|
-
* wrapping `canon` in a function flattens every status to zero.
|
|
41
|
-
*
|
|
40
|
+
* wrapping `canon` in a function flattens every status to zero. The base
|
|
41
|
+
* stack's hook, and any hook installed before `canon hooks post-merge`, reads
|
|
42
|
+
* the record with a pattern rather than a parser, so every field it branches on is
|
|
42
43
|
* a number or a fixed word, and `outcomes` sits last so a greedy pattern
|
|
43
44
|
* anchored on a scalar never reaches into it.
|
|
44
45
|
*/
|
package/src/counts/catalogs.ts
CHANGED
|
@@ -25,14 +25,14 @@ const REGISTER_IMPORT = /^import \{ register as /gm
|
|
|
25
25
|
/**
|
|
26
26
|
* Every top-level `canon` command, read off the CLI entry point rather than a
|
|
27
27
|
* catalog verb, because a command carries no `--json` listing of its own
|
|
28
|
-
* siblings. `regen-hero.
|
|
28
|
+
* siblings. `regen-hero.ts` imports this reader for its `COMMAND_COUNT`
|
|
29
29
|
* rather than a second regex on `src/cli.ts`, so this is the one place that
|
|
30
30
|
* pattern is written.
|
|
31
31
|
*
|
|
32
32
|
* This is the one catalog with no meaning outside this repository, the way
|
|
33
|
-
* `regen-hero.
|
|
33
|
+
* `regen-hero.ts` documents itself as clone-only for the same reason.
|
|
34
34
|
*/
|
|
35
|
-
function countCommands(root: string): number | undefined {
|
|
35
|
+
export function countCommands(root: string): number | undefined {
|
|
36
36
|
const cli = join(root, 'src', 'cli.ts')
|
|
37
37
|
if (!existsSync(cli)) return undefined
|
|
38
38
|
return [...readFileSync(cli, 'utf8').matchAll(REGISTER_IMPORT)].length
|
package/src/design/tokens.ts
CHANGED
|
@@ -99,7 +99,7 @@ export const TOKENS: DesignTokens = {
|
|
|
99
99
|
'',
|
|
100
100
|
'The values below are the system rather than a reading of one. Until 2026-09-01 this record transcribed two surfaces and agreed with nothing else, which is what made a change to it reach nobody. The slide theme, the token preview, and a teach workspace stylesheet now read the module this file is rendered from, so a value changed there changes what all three render.',
|
|
101
101
|
'',
|
|
102
|
-
'The two rendered captures read it as well. `scripts/core/regen-hero.
|
|
102
|
+
'The two rendered captures read it as well. `scripts/core/regen-hero.ts` fills `assets/captures/hero.html.tmpl` and `assets/captures/install.html.tmpl` with what `canon design css --no-components` emits, so both frames now carry the custom properties rather than their own copies of the hex, and a value moved here moves what the next capture renders.',
|
|
103
103
|
'',
|
|
104
104
|
'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/context/scripts/framing.md` records one color source per language with a check behind each, so generating a third spelling from here would break the rule those two checks enforce. What the record is still incomplete about is the other half of those six: `WHITE` and `GREY` name no role below, so the terminal palette is described here in part rather than in whole.',
|
|
105
105
|
].join('\n'),
|