@erclx/canon 4.118.0 → 4.120.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/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-rollout/SKILL.md +2 -2
- package/claude/skills/create-skill/SKILL.md +2 -1
- package/claude/skills/design-extract/SKILL.md +1 -1
- package/claude/skills/docs-fold/SKILL.md +2 -2
- package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +1 -1
- package/claude/skills/git-pr/REQUIREMENT.md +1 -0
- package/claude/skills/git-pr/SKILL.md +39 -7
- package/claude/skills/memory-capture/SKILL.md +3 -3
- package/claude/skills/memory-review/SKILL.md +3 -3
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/review-branch/SKILL.md +1 -1
- package/claude/skills/review-pr/SKILL.md +13 -242
- package/claude/skills/review-pr/references/close-out.md +30 -0
- package/claude/skills/review-pr/references/post.md +145 -0
- package/claude/skills/review-pr/references/scope.md +82 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +9 -16
- package/claude/skills/role-orchestrator/SKILL.md +9 -153
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -193
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +22 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +85 -0
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +61 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +56 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-fallback.md +18 -0
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +1 -1
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +10 -41
- package/claude/skills/seed-sync/SKILL.md +1 -1
- package/claude/skills/session-compact/SKILL.md +1 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-relay/REQUIREMENT.md +7 -1
- package/claude/skills/session-relay/SKILL.md +4 -7
- package/claude/skills/session-worktree/REQUIREMENT.md +5 -1
- package/claude/skills/session-worktree/SKILL.md +7 -1
- package/claude/skills/session-worktree/references/main-root-writes.md +24 -0
- package/claude/skills/task-board/SKILL.md +4 -3
- package/claude/skills/teach-workspace/SKILL.md +4 -4
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/claude/skills/ux-walkthrough/references/relay.md +1 -1
- package/docs/agents/commands.md +4 -3
- package/docs/agents/counts.md +1 -1
- package/docs/agents/index.md +8 -3
- package/docs/agents/install-and-sync.md +11 -6
- package/docs/agents/pr-evidence.md +19 -3
- package/docs/agents/pr-local.md +102 -0
- package/docs/agents/pr-preview.md +2 -2
- package/docs/agents/records-push.md +61 -0
- package/docs/agents/records-validate.md +73 -0
- package/docs/agents/records.md +3 -119
- package/docs/agents/sessions-transfer.md +91 -0
- package/docs/agents/sessions.md +3 -82
- package/docs/agents/skills-audit.md +2 -2
- package/docs/agents/tasks.md +4 -4
- package/docs/agents/teach-authoring.md +112 -0
- package/docs/agents/teach.md +3 -103
- package/docs/target-projects.md +2 -2
- package/docs/workflow/ai-workflow.md +10 -10
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/renames.toml +13 -0
- package/governance/rules/claude/570-skill.md +2 -1
- package/governance/rules/core/085-worktrees.md +2 -8
- package/package.json +1 -1
- package/scripts/core/check-ignore-parity.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -1
- package/src/claude/cases/workflow.ts +10 -0
- package/src/commands/pr.ts +324 -2
- package/src/commands/sync.ts +11 -3
- package/src/commands/tasks.ts +9 -8
- package/src/context/classify/extract.ts +2 -2
- package/src/counts/scan.ts +1 -1
- package/src/design/base.css +19 -19
- package/src/design/board.ts +18 -7
- package/src/design/neutral.ts +127 -0
- package/src/design/regen.ts +6 -4
- package/src/design/tokens.ts +6 -3
- package/src/gate/stages.ts +6 -6
- package/src/gov/adapter.ts +51 -1
- package/src/gov/install.ts +13 -3
- package/src/gov/renames.ts +76 -0
- package/src/gov/stacks.ts +5 -1
- package/src/pr/evidence.ts +42 -2
- package/src/pr/local.ts +125 -0
- package/src/sync/check.ts +19 -4
- package/src/sync/engine.ts +67 -3
- package/src/tasks/archive.ts +28 -7
- package/src/tasks/record.ts +16 -9
- package/src/tasks/validate.ts +3 -1
- package/standards/index.md +1 -0
- package/standards/skill-requirement.md +72 -0
- package/standards/skill.md +34 -152
- package/standards/tasks.md +5 -3
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +32 -0
- package/governance/rules/core/091-channel.md +0 -13
package/src/commands/pr.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { readdir, readFile, readlink, realpath } from 'node:fs/promises'
|
|
3
3
|
import { basename, join, resolve } from 'node:path'
|
|
4
4
|
import { $ } from 'bun'
|
|
5
5
|
import type { Command } from 'commander'
|
|
@@ -28,11 +28,21 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
findEvidenceChecklist,
|
|
30
30
|
findEvidenceCommentId,
|
|
31
|
+
findEvidenceLocal,
|
|
31
32
|
findEvidencePreview,
|
|
32
33
|
groupEvidence,
|
|
33
34
|
renderEvidenceBody,
|
|
34
35
|
} from '@/pr/evidence'
|
|
35
36
|
import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
|
|
37
|
+
import {
|
|
38
|
+
findLocalServer,
|
|
39
|
+
type Listener,
|
|
40
|
+
type LocalRefusal,
|
|
41
|
+
type LocalRunner,
|
|
42
|
+
parseLsofListeners,
|
|
43
|
+
parseProcNetTcp,
|
|
44
|
+
stripLocalLine,
|
|
45
|
+
} from '@/pr/local'
|
|
36
46
|
import { KEY_CHANGES } from '@/pr/paths'
|
|
37
47
|
import {
|
|
38
48
|
findDeployWorkflow,
|
|
@@ -76,6 +86,28 @@ interface ReadOptions {
|
|
|
76
86
|
interface EvidenceOptions extends ReadOptions {
|
|
77
87
|
readonly preview?: string
|
|
78
88
|
readonly checklist?: string
|
|
89
|
+
readonly local?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface LocalOptions extends ReadOptions {
|
|
93
|
+
readonly remove?: boolean
|
|
94
|
+
readonly note?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What replaces the local address line once the pull request closes. */
|
|
98
|
+
const LOCAL_REMOVED_NOTE =
|
|
99
|
+
'_Local preview removed when the pull request closed._'
|
|
100
|
+
|
|
101
|
+
/** How long the probe waits on a listener before reading it as not a server. */
|
|
102
|
+
const LOCAL_PROBE_TIMEOUT_MS = 2_000
|
|
103
|
+
|
|
104
|
+
const LOCAL_REFUSALS: Record<LocalRefusal | 'gh-failed', string> = {
|
|
105
|
+
'no-server':
|
|
106
|
+
'Nothing listening inside this worktree served an HTML page. Start the dev server here and re-run to get a link.',
|
|
107
|
+
'no-listener-reader':
|
|
108
|
+
'This machine offers neither lsof nor /proc, so no listening socket could be read. Nothing is posted.',
|
|
109
|
+
'gh-failed':
|
|
110
|
+
'gh could not read or edit the marked comment on this pull request.',
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
interface PreviewOptions extends ReadOptions {
|
|
@@ -387,6 +419,10 @@ export function register(program: Command): void {
|
|
|
387
419
|
'--checklist <path>',
|
|
388
420
|
'Close the body with this visual checklist file, from canon:ui-checklist',
|
|
389
421
|
)
|
|
422
|
+
.option(
|
|
423
|
+
'--local <url>',
|
|
424
|
+
"Add this worktree's running server under any preview line, from canon pr local",
|
|
425
|
+
)
|
|
390
426
|
.addHelpText(
|
|
391
427
|
'after',
|
|
392
428
|
[
|
|
@@ -419,6 +455,13 @@ export function register(program: Command): void {
|
|
|
419
455
|
'image still reports no-evidence, and the caller posts the checklist',
|
|
420
456
|
'on its own.',
|
|
421
457
|
'',
|
|
458
|
+
'--local adds a **Local preview:** line under the hosted preview line,',
|
|
459
|
+
'or opens the body with it when there is none, and is carried forward',
|
|
460
|
+
'the same way. It does not by itself turn no-evidence into ok, so a',
|
|
461
|
+
'branch with a server running and nothing to show posts no link-only',
|
|
462
|
+
'comment. Together with --checklist it does, so the checklist and the',
|
|
463
|
+
'link land in one comment.',
|
|
464
|
+
'',
|
|
422
465
|
'Exit codes:',
|
|
423
466
|
' 0 read, whether it produced a body or reported no-evidence',
|
|
424
467
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
@@ -428,6 +471,7 @@ export function register(program: Command): void {
|
|
|
428
471
|
' canon pr evidence 1341 --json',
|
|
429
472
|
' canon pr evidence 1341 --preview https://feat-x.site.pages.dev --json',
|
|
430
473
|
' canon pr evidence 1341 --checklist .canon/tmp/handoff/ui-checklist/x.md --json',
|
|
474
|
+
' canon pr evidence 1341 --local http://localhost:5173 --json',
|
|
431
475
|
'',
|
|
432
476
|
].join('\n'),
|
|
433
477
|
)
|
|
@@ -484,6 +528,66 @@ export function register(program: Command): void {
|
|
|
484
528
|
.action(async (number: string | undefined, opts: PreviewOptions) => {
|
|
485
529
|
process.exitCode = await runPreview(number, opts)
|
|
486
530
|
})
|
|
531
|
+
|
|
532
|
+
pr.command('local')
|
|
533
|
+
.description(
|
|
534
|
+
"Find the server this worktree is running and report its localhost address, or remove it from the pull request's evidence comment",
|
|
535
|
+
)
|
|
536
|
+
.argument('[number]', 'Pull request to edit on --remove')
|
|
537
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
538
|
+
.option('--root <path>', 'Worktree to read, defaulting to the cwd')
|
|
539
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
540
|
+
.option(
|
|
541
|
+
'--remove',
|
|
542
|
+
"Replace the evidence comment's local preview line with a note",
|
|
543
|
+
)
|
|
544
|
+
.option(
|
|
545
|
+
'--note <text>',
|
|
546
|
+
'What replaces the line on --remove',
|
|
547
|
+
LOCAL_REMOVED_NOTE,
|
|
548
|
+
)
|
|
549
|
+
.addHelpText(
|
|
550
|
+
'after',
|
|
551
|
+
[
|
|
552
|
+
'',
|
|
553
|
+
'Reads every TCP socket in LISTEN, keeps those whose owning process runs',
|
|
554
|
+
"inside this worktree's toplevel, and requests / on each. The lowest",
|
|
555
|
+
'port serving an HTML page is reported, which skips a test runner or a',
|
|
556
|
+
'reload socket answering in the same tree.',
|
|
557
|
+
'',
|
|
558
|
+
'A process in a sibling worktree, in the main checkout seen from a',
|
|
559
|
+
'linked worktree, or in a linked worktree seen from the main checkout',
|
|
560
|
+
'is never reported, since its server shows another branch. Sockets are',
|
|
561
|
+
'read through lsof, or through /proc on a Linux machine without it.',
|
|
562
|
+
'',
|
|
563
|
+
'--remove reads the pull request comment carrying the pr-evidence',
|
|
564
|
+
'marker and replaces its **Local preview:** line with --note, editing',
|
|
565
|
+
'the comment itself so a close workflow with no session can call it.',
|
|
566
|
+
'',
|
|
567
|
+
'Read `reason` on the JSON record:',
|
|
568
|
+
' ok a page answered, with its address in `url`',
|
|
569
|
+
' no-server nothing inside this worktree served a page',
|
|
570
|
+
' no-listener-reader neither lsof nor /proc is available',
|
|
571
|
+
' removed --remove replaced the line',
|
|
572
|
+
' no-comment --remove found no marked comment, a no-op',
|
|
573
|
+
' no-line --remove found no local line, a no-op',
|
|
574
|
+
'',
|
|
575
|
+
'Exit codes:',
|
|
576
|
+
' 0 a server was found, or --remove finished, including a no-op',
|
|
577
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
578
|
+
'',
|
|
579
|
+
'Examples:',
|
|
580
|
+
' canon pr local --json',
|
|
581
|
+
' canon pr local 1341 --remove --json',
|
|
582
|
+
'',
|
|
583
|
+
].join('\n'),
|
|
584
|
+
)
|
|
585
|
+
.action(async (number: string | undefined, opts: LocalOptions) => {
|
|
586
|
+
process.exitCode =
|
|
587
|
+
opts.remove === true
|
|
588
|
+
? await runLocalRemove(number, opts)
|
|
589
|
+
: await runLocal(opts)
|
|
590
|
+
})
|
|
487
591
|
}
|
|
488
592
|
|
|
489
593
|
interface PullRequestRead {
|
|
@@ -1319,7 +1423,15 @@ async function runEvidence(
|
|
|
1319
1423
|
baseTree.has(path),
|
|
1320
1424
|
)
|
|
1321
1425
|
|
|
1322
|
-
|
|
1426
|
+
// A local address rides on a checklist or a hosted preview and never opens
|
|
1427
|
+
// a comment alone, so a docs-only branch with a server up posts nothing.
|
|
1428
|
+
const hasChecklistAndLocal =
|
|
1429
|
+
suppliedChecklist !== undefined && opts.local !== undefined
|
|
1430
|
+
if (
|
|
1431
|
+
grouped.kind === 'refused' &&
|
|
1432
|
+
opts.preview === undefined &&
|
|
1433
|
+
!hasChecklistAndLocal
|
|
1434
|
+
) {
|
|
1323
1435
|
logStep('Skipped')
|
|
1324
1436
|
logInfo(
|
|
1325
1437
|
'No changed path carries an evidence/ segment, so there is nothing to post.',
|
|
@@ -1355,6 +1467,7 @@ async function runEvidence(
|
|
|
1355
1467
|
let commentId: number | undefined
|
|
1356
1468
|
let carriedPreview: string | undefined
|
|
1357
1469
|
let carriedChecklist: string | undefined
|
|
1470
|
+
let carriedLocal: string | undefined
|
|
1358
1471
|
if (identity.number !== undefined) {
|
|
1359
1472
|
const commentsRow = await gh(root, [
|
|
1360
1473
|
'pr',
|
|
@@ -1389,9 +1502,11 @@ async function runEvidence(
|
|
|
1389
1502
|
commentId = findEvidenceCommentId(comments)
|
|
1390
1503
|
carriedPreview = findEvidencePreview(comments)
|
|
1391
1504
|
carriedChecklist = findEvidenceChecklist(comments)
|
|
1505
|
+
carriedLocal = findEvidenceLocal(comments)
|
|
1392
1506
|
}
|
|
1393
1507
|
|
|
1394
1508
|
const preview = opts.preview ?? carriedPreview
|
|
1509
|
+
const local = opts.local ?? carriedLocal
|
|
1395
1510
|
const checklist = suppliedChecklist ?? carriedChecklist
|
|
1396
1511
|
const states = grouped.kind === 'read' ? grouped.states : []
|
|
1397
1512
|
const body = renderEvidenceBody(
|
|
@@ -1401,6 +1516,7 @@ async function runEvidence(
|
|
|
1401
1516
|
identity.head,
|
|
1402
1517
|
preview,
|
|
1403
1518
|
checklist,
|
|
1519
|
+
local,
|
|
1404
1520
|
)
|
|
1405
1521
|
|
|
1406
1522
|
const caseCount = states.reduce((n, s) => n + s.items.length, 0)
|
|
@@ -1410,6 +1526,7 @@ async function runEvidence(
|
|
|
1410
1526
|
`${plural(caseCount, 'case')} across ${plural(states.length, 'state')}`,
|
|
1411
1527
|
)
|
|
1412
1528
|
if (preview !== undefined) logInfo(`preview ${preview}`)
|
|
1529
|
+
if (local !== undefined) logInfo(`local preview ${local}`)
|
|
1413
1530
|
if (checklist !== undefined) {
|
|
1414
1531
|
logInfo(
|
|
1415
1532
|
suppliedChecklist === undefined
|
|
@@ -1603,6 +1720,211 @@ async function runPreview(
|
|
|
1603
1720
|
return 0
|
|
1604
1721
|
}
|
|
1605
1722
|
|
|
1723
|
+
async function readText(path: string): Promise<string | undefined> {
|
|
1724
|
+
return readFile(path, 'utf8').catch(() => undefined)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Every listening socket's owning pid, read from `/proc`. Only a process this
|
|
1729
|
+
* user owns exposes its `fd` table, which is also the only process whose
|
|
1730
|
+
* server this user's worktree could have started.
|
|
1731
|
+
*/
|
|
1732
|
+
async function readProcListeners(): Promise<Listener[] | undefined> {
|
|
1733
|
+
const tables = await Promise.all(
|
|
1734
|
+
['/proc/net/tcp', '/proc/net/tcp6'].map(readText),
|
|
1735
|
+
)
|
|
1736
|
+
if (tables.every((text) => text === undefined)) return undefined
|
|
1737
|
+
const byInode = new Map(
|
|
1738
|
+
tables.flatMap((text) =>
|
|
1739
|
+
text === undefined
|
|
1740
|
+
? []
|
|
1741
|
+
: parseProcNetTcp(text).map((s) => [s.inode, s.port] as const),
|
|
1742
|
+
),
|
|
1743
|
+
)
|
|
1744
|
+
|
|
1745
|
+
const processIds = (await readdir('/proc').catch(() => [])).filter((name) =>
|
|
1746
|
+
/^\d+$/.test(name),
|
|
1747
|
+
)
|
|
1748
|
+
const listeners: Listener[] = []
|
|
1749
|
+
for (const pid of processIds) {
|
|
1750
|
+
const fds = await readdir(`/proc/${pid}/fd`).catch(() => [])
|
|
1751
|
+
for (const fd of fds) {
|
|
1752
|
+
const target = await readlink(`/proc/${pid}/fd/${fd}`).catch(() => '')
|
|
1753
|
+
const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1]
|
|
1754
|
+
const port = inode === undefined ? undefined : byInode.get(inode)
|
|
1755
|
+
if (port !== undefined) listeners.push({ pid: Number(pid), port })
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return listeners
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* The machine half of a detection: lsof where it is installed, `/proc` on a
|
|
1763
|
+
* Linux machine without it, and no reader anywhere else.
|
|
1764
|
+
*/
|
|
1765
|
+
function machineLocalRunner(): LocalRunner {
|
|
1766
|
+
const hasLsof = Bun.which('lsof') !== null
|
|
1767
|
+
return {
|
|
1768
|
+
async listListeners() {
|
|
1769
|
+
if (hasLsof) {
|
|
1770
|
+
const result = await $`lsof -nP -iTCP -sTCP:LISTEN -Fpn`
|
|
1771
|
+
.quiet()
|
|
1772
|
+
.nothrow()
|
|
1773
|
+
// lsof exits 1 both when it matched nothing and when it could not
|
|
1774
|
+
// read some process, and the rows it did print are real either way.
|
|
1775
|
+
if (result.exitCode <= 1) return parseLsofListeners(result.text())
|
|
1776
|
+
}
|
|
1777
|
+
if (existsSync('/proc/net/tcp')) return readProcListeners()
|
|
1778
|
+
return undefined
|
|
1779
|
+
},
|
|
1780
|
+
async cwdOf(pid) {
|
|
1781
|
+
if (existsSync(`/proc/${pid}/cwd`)) {
|
|
1782
|
+
return readlink(`/proc/${pid}/cwd`).catch(() => undefined)
|
|
1783
|
+
}
|
|
1784
|
+
if (!hasLsof) return undefined
|
|
1785
|
+
const result = await $`lsof -a -p ${pid} -d cwd -Fn`.quiet().nothrow()
|
|
1786
|
+
const line = result
|
|
1787
|
+
.text()
|
|
1788
|
+
.split('\n')
|
|
1789
|
+
.find((row) => row.startsWith('n'))
|
|
1790
|
+
return line?.slice(1)
|
|
1791
|
+
},
|
|
1792
|
+
// An HTML page rather than any answer, since a test runner's server or a
|
|
1793
|
+
// reload socket in the same tree answers too, and a reviewer opening one
|
|
1794
|
+
// gets a bare 404 instead of the branch.
|
|
1795
|
+
async probe(port) {
|
|
1796
|
+
try {
|
|
1797
|
+
const response = await fetch(`http://localhost:${port}/`, {
|
|
1798
|
+
signal: AbortSignal.timeout(LOCAL_PROBE_TIMEOUT_MS),
|
|
1799
|
+
})
|
|
1800
|
+
await response.body?.cancel()
|
|
1801
|
+
return (response.headers.get('content-type') ?? '').includes(
|
|
1802
|
+
'text/html',
|
|
1803
|
+
)
|
|
1804
|
+
} catch {
|
|
1805
|
+
return false
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** The worktree's toplevel with symlinks resolved, since a process cwd is read back resolved. */
|
|
1812
|
+
async function resolveWorktreeRoot(root: string): Promise<string> {
|
|
1813
|
+
const result = await $`git -C ${root} rev-parse --show-toplevel`
|
|
1814
|
+
.env(gitEnv())
|
|
1815
|
+
.quiet()
|
|
1816
|
+
.nothrow()
|
|
1817
|
+
const toplevel = result.exitCode === 0 ? result.text().trim() : root
|
|
1818
|
+
return realpath(toplevel).catch(() => toplevel)
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
async function runLocal(opts: LocalOptions): Promise<number> {
|
|
1822
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
1823
|
+
const emitJson = opts.json ?? false
|
|
1824
|
+
|
|
1825
|
+
intro('canon pr local')
|
|
1826
|
+
|
|
1827
|
+
const worktree = await resolveWorktreeRoot(root)
|
|
1828
|
+
const result = await findLocalServer(worktree, machineLocalRunner())
|
|
1829
|
+
if (result.kind === 'refused') {
|
|
1830
|
+
return refuseWith(
|
|
1831
|
+
result.reason,
|
|
1832
|
+
LOCAL_REFUSALS[result.reason],
|
|
1833
|
+
emitJson,
|
|
1834
|
+
root,
|
|
1835
|
+
)
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
logStep('Local preview')
|
|
1839
|
+
logInfo(result.url)
|
|
1840
|
+
outro()
|
|
1841
|
+
|
|
1842
|
+
if (emitJson) {
|
|
1843
|
+
process.stdout.write(
|
|
1844
|
+
`${JSON.stringify({ root, reason: 'ok', url: result.url, port: result.port })}\n`,
|
|
1845
|
+
)
|
|
1846
|
+
}
|
|
1847
|
+
return 0
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Replaces the local line on the marked comment. The verb writes here rather
|
|
1852
|
+
* than handing a body back, because its caller is a close workflow with no
|
|
1853
|
+
* session to post one.
|
|
1854
|
+
*/
|
|
1855
|
+
async function runLocalRemove(
|
|
1856
|
+
number: string | undefined,
|
|
1857
|
+
opts: LocalOptions,
|
|
1858
|
+
): Promise<number> {
|
|
1859
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
1860
|
+
const emitJson = opts.json ?? false
|
|
1861
|
+
const note = opts.note ?? LOCAL_REMOVED_NOTE
|
|
1862
|
+
|
|
1863
|
+
intro('canon pr local --remove')
|
|
1864
|
+
|
|
1865
|
+
if (Bun.which('gh') === null) {
|
|
1866
|
+
return refuseWith('gh-missing', PULL_REFUSALS['gh-missing'], emitJson, root)
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const args = ['pr', 'view']
|
|
1870
|
+
if (number !== undefined) args.push(number)
|
|
1871
|
+
args.push('--json', 'comments')
|
|
1872
|
+
const row = await gh(root, args)
|
|
1873
|
+
let comments: readonly { url?: string; body: string }[] | undefined
|
|
1874
|
+
try {
|
|
1875
|
+
comments =
|
|
1876
|
+
row === null
|
|
1877
|
+
? undefined
|
|
1878
|
+
: (
|
|
1879
|
+
JSON.parse(row) as {
|
|
1880
|
+
comments?: readonly { url?: string; body: string }[]
|
|
1881
|
+
}
|
|
1882
|
+
).comments
|
|
1883
|
+
} catch {
|
|
1884
|
+
comments = undefined
|
|
1885
|
+
}
|
|
1886
|
+
if (comments === undefined) {
|
|
1887
|
+
return refuseWith('gh-failed', LOCAL_REFUSALS['gh-failed'], emitJson, root)
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const finish = (reason: string, message: string): number => {
|
|
1891
|
+
logStep(reason === 'removed' ? 'Removed' : 'Skipped')
|
|
1892
|
+
logInfo(message)
|
|
1893
|
+
outro()
|
|
1894
|
+
if (emitJson) {
|
|
1895
|
+
process.stdout.write(`${JSON.stringify({ root, reason })}\n`)
|
|
1896
|
+
}
|
|
1897
|
+
return 0
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const commentId = findEvidenceCommentId(comments)
|
|
1901
|
+
const marked = comments.find((comment) =>
|
|
1902
|
+
comment.url?.endsWith(`#issuecomment-${commentId}`),
|
|
1903
|
+
)
|
|
1904
|
+
if (commentId === undefined || marked === undefined) {
|
|
1905
|
+
return finish('no-comment', 'No comment carries the evidence marker.')
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const stripped = stripLocalLine(marked.body, note)
|
|
1909
|
+
if (stripped === undefined) {
|
|
1910
|
+
return finish('no-line', 'The evidence comment carries no local line.')
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const patched = await gh(root, [
|
|
1914
|
+
'api',
|
|
1915
|
+
'-X',
|
|
1916
|
+
'PATCH',
|
|
1917
|
+
`repos/{owner}/{repo}/issues/comments/${commentId}`,
|
|
1918
|
+
'-f',
|
|
1919
|
+
`body=${stripped}`,
|
|
1920
|
+
])
|
|
1921
|
+
if (patched === null) {
|
|
1922
|
+
return refuseWith('gh-failed', LOCAL_REFUSALS['gh-failed'], emitJson, root)
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
return finish('removed', `comment ${commentId}`)
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1606
1928
|
/**
|
|
1607
1929
|
* Frames a refusal on stderr in both modes and puts the record on stdout alone,
|
|
1608
1930
|
* so an operator reading the terminal sees the reason rather than a command
|
package/src/commands/sync.ts
CHANGED
|
@@ -133,9 +133,17 @@ function renderCheck(report: CheckReport): void {
|
|
|
133
133
|
logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const {
|
|
137
|
+
stale,
|
|
138
|
+
customized,
|
|
139
|
+
drifted,
|
|
140
|
+
stranded,
|
|
141
|
+
retired,
|
|
142
|
+
renamed,
|
|
143
|
+
orphaned,
|
|
144
|
+
missing,
|
|
145
|
+
} = domain.counts
|
|
146
|
+
if (stale + customized + drifted + stranded + retired + renamed === 0) {
|
|
139
147
|
logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
|
|
140
148
|
}
|
|
141
149
|
// A local file is not a deficiency and a missing rule is, so this never
|
package/src/commands/tasks.ts
CHANGED
|
@@ -409,12 +409,14 @@ export function register(program: Command): void {
|
|
|
409
409
|
[
|
|
410
410
|
'',
|
|
411
411
|
'Exit codes:',
|
|
412
|
-
' 0 the line was added, corrected, or already correct',
|
|
412
|
+
' 0 the line was added, appended to, corrected, or already correct',
|
|
413
413
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
414
414
|
'',
|
|
415
415
|
'It adds Pull request: #NNN under the origin lines the task carries,',
|
|
416
|
-
'and
|
|
417
|
-
'
|
|
416
|
+
'and appends the number to the line when one exists, so a task shipped',
|
|
417
|
+
'in slices lists every pull request. A line that does not read as a',
|
|
418
|
+
'list is replaced. The board is shared scratch, so a linked worktree',
|
|
419
|
+
'records against the same board.',
|
|
418
420
|
'',
|
|
419
421
|
'Examples:',
|
|
420
422
|
' canon tasks pull-request 673 v28.1-trigger-escalation',
|
|
@@ -448,11 +450,10 @@ export function register(program: Command): void {
|
|
|
448
450
|
' 0 the line was added, corrected, or already correct',
|
|
449
451
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
450
452
|
'',
|
|
451
|
-
'It writes Plan: [<label>](<target>) right after the H1, the
|
|
452
|
-
'
|
|
453
|
-
'
|
|
454
|
-
'
|
|
455
|
-
'resolves the board root in-process.',
|
|
453
|
+
'It writes Plan: [<label>](<target>) right after the H1, corrects the',
|
|
454
|
+
'target in place when the line exists, and resolves both a bare slug',
|
|
455
|
+
'and a board-relative path the way canon tasks plan-answers does. Safe',
|
|
456
|
+
'from a linked worktree, since it resolves the board root in-process.',
|
|
456
457
|
'',
|
|
457
458
|
'Examples:',
|
|
458
459
|
' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
|
|
@@ -285,8 +285,8 @@ export function sweepSections(
|
|
|
285
285
|
// An H3 sitting before the document's first H2, or in a document that
|
|
286
286
|
// carries no H2 at all, has no enclosing section for the main loop below
|
|
287
287
|
// to attribute it to. Sweep it as a section of its own instead of
|
|
288
|
-
// dropping it, which is the shape
|
|
289
|
-
//
|
|
288
|
+
// dropping it, which is the shape of an entry whose H1 is followed
|
|
289
|
+
// directly by H3s with no H2 anywhere.
|
|
290
290
|
const firstH2 = headings.findIndex((h) => h.level === 2)
|
|
291
291
|
const leadingBound = firstH2 === -1 ? headings.length : firstH2
|
|
292
292
|
|
package/src/counts/scan.ts
CHANGED
|
@@ -129,7 +129,7 @@ const QUANTIFIERS = ['all']
|
|
|
129
129
|
* of the number.
|
|
130
130
|
*
|
|
131
131
|
* Found on this module's own first review, against a live instance the verb
|
|
132
|
-
* gate alone reported clean:
|
|
132
|
+
* gate alone reported clean: the development gating-stages entry stated
|
|
133
133
|
* `the twelve audits` while the tree held 20, sitting inside the plausibility
|
|
134
134
|
* bound this design already carries, so nothing but the missing shape kept it
|
|
135
135
|
* from reporting.
|
package/src/design/base.css
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/* Generated by `canon design regen` from src/design/
|
|
1
|
+
/* Generated by `canon design regen` from src/design/neutral.ts. Do not edit.
|
|
2
2
|
This is the base half of a design install. A target overrides a value in
|
|
3
3
|
.claude/design/project/, which the three-way merge leaves alone, so this
|
|
4
4
|
file changes freely and nothing a project wrote lives in it. */
|
|
5
5
|
|
|
6
6
|
:root {
|
|
7
|
-
--color-background: #
|
|
8
|
-
--color-surface: #
|
|
9
|
-
--color-chrome: #
|
|
10
|
-
--color-border: #
|
|
11
|
-
--color-text: #
|
|
12
|
-
--color-text-body: #
|
|
13
|
-
--color-text-secondary: #
|
|
14
|
-
--color-muted: #
|
|
15
|
-
--color-accent: #
|
|
7
|
+
--color-background: #0e0e0e;
|
|
8
|
+
--color-surface: #141414;
|
|
9
|
+
--color-chrome: #1a1a1a;
|
|
10
|
+
--color-border: #292929;
|
|
11
|
+
--color-text: #dddddd;
|
|
12
|
+
--color-text-body: #b2b2b2;
|
|
13
|
+
--color-text-secondary: #8e8e8e;
|
|
14
|
+
--color-muted: #7f7f7f;
|
|
15
|
+
--color-accent: #ececec;
|
|
16
16
|
--color-success: #61c454;
|
|
17
|
-
--color-light-background: #
|
|
18
|
-
--color-light-surface: #
|
|
19
|
-
--color-light-chrome: #
|
|
20
|
-
--color-light-text: #
|
|
21
|
-
--color-light-text-body: #
|
|
22
|
-
--color-light-text-secondary: #
|
|
23
|
-
--color-light-muted: #
|
|
24
|
-
--color-light-accent: #
|
|
17
|
+
--color-light-background: #fafafa;
|
|
18
|
+
--color-light-surface: #f1f1f1;
|
|
19
|
+
--color-light-chrome: #e8e8e8;
|
|
20
|
+
--color-light-text: #252525;
|
|
21
|
+
--color-light-text-body: #444444;
|
|
22
|
+
--color-light-text-secondary: #5f5f5f;
|
|
23
|
+
--color-light-muted: #6c6c6c;
|
|
24
|
+
--color-light-accent: #191919;
|
|
25
25
|
--color-light-success: #2d6b22;
|
|
26
|
-
--color-light-border: #
|
|
26
|
+
--color-light-border: #d4d4d4;
|
|
27
27
|
--space-xs: 0.25rem;
|
|
28
28
|
--space-sm: 0.5rem;
|
|
29
29
|
--space-md: 0.875rem;
|
package/src/design/board.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { join, relative, sep } from 'node:path'
|
|
11
11
|
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
12
12
|
import { renderDesignDoc } from '@/design/render'
|
|
13
|
+
import { NEUTRAL_TOKENS } from '@/design/neutral'
|
|
13
14
|
import { colorValue } from '@/design/tokens'
|
|
14
15
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
15
16
|
import { recordDir } from '@/record-root'
|
|
@@ -131,6 +132,16 @@ const SCROLLSPY_SCRIPT = [
|
|
|
131
132
|
'})();</script>',
|
|
132
133
|
].join('')
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* A role's toolkit value, falling back to the neutral install base's where the
|
|
137
|
+
* toolkit record drops the role, so the board never restates a hex of its own.
|
|
138
|
+
*/
|
|
139
|
+
function roleColor(role: string): string {
|
|
140
|
+
const value = colorValue(role) ?? colorValue(role, NEUTRAL_TOKENS)
|
|
141
|
+
if (value === undefined) throw new Error(`No design token declares ${role}`)
|
|
142
|
+
return value
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
/**
|
|
135
146
|
* The same brand mark `src/design/render.ts` embeds in its own preview,
|
|
136
147
|
* colored with the dark accent since the board's default chrome is dark
|
|
@@ -139,7 +150,7 @@ const SCROLLSPY_SCRIPT = [
|
|
|
139
150
|
* copies rather than a shared icon helper for the first second caller.
|
|
140
151
|
*/
|
|
141
152
|
function faviconLink(): string {
|
|
142
|
-
const color =
|
|
153
|
+
const color = roleColor('accent')
|
|
143
154
|
const href = `data:image/svg+xml,${encodeURIComponent(
|
|
144
155
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${color}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${color}" /></svg>`,
|
|
145
156
|
)}`
|
|
@@ -232,12 +243,12 @@ ${SCROLLSPY_SCRIPT}
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
function panelPage(title: string, body: string): string {
|
|
235
|
-
const accent =
|
|
236
|
-
const surface =
|
|
237
|
-
const border =
|
|
238
|
-
const muted =
|
|
239
|
-
const text =
|
|
240
|
-
const background =
|
|
246
|
+
const accent = roleColor('light-accent')
|
|
247
|
+
const surface = roleColor('light-surface')
|
|
248
|
+
const border = roleColor('light-border')
|
|
249
|
+
const muted = roleColor('light-muted')
|
|
250
|
+
const text = roleColor('light-text')
|
|
251
|
+
const background = roleColor('light-background')
|
|
241
252
|
|
|
242
253
|
return `<!doctype html>
|
|
243
254
|
<html lang="en">
|