@erclx/canon 4.107.0 → 4.109.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 +15 -30
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/document-health/REQUIREMENT.md +46 -0
- package/claude/skills/document-health/SKILL.md +88 -0
- package/claude/skills/document-health/references/axes.md +78 -0
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/review-address/SKILL.md +4 -3
- package/claude/skills/task-board/REQUIREMENT.md +1 -1
- package/claude/skills/task-board/SKILL.md +2 -2
- package/docs/agents/capture.md +3 -3
- package/docs/agents/commands.md +2 -2
- package/docs/agents/pr-reads.md +2 -0
- package/docs/agents/tasks.md +10 -8
- package/docs/workflow/ai-workflow.md +1 -0
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +2 -34
- package/src/claude/cases/workflow.ts +10 -0
- package/src/cli.ts +2 -161
- package/src/commands/pr.ts +29 -4
- package/src/commands/tasks.ts +22 -7
- package/src/help.ts +196 -0
- package/src/pr/checks.ts +29 -0
- package/src/tasks/label.ts +136 -29
- package/standards/readme.md +1 -0
package/src/cli.ts
CHANGED
|
@@ -43,169 +43,10 @@ import { register as census } from '@/commands/census'
|
|
|
43
43
|
import { register as targets } from '@/commands/targets'
|
|
44
44
|
import { register as upgrade } from '@/commands/upgrade'
|
|
45
45
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
const HELP_WIDTH = 80
|
|
49
|
-
const SIGNATURE_WIDTH = 19
|
|
50
|
-
const ROW_INDENT = '│ '.length
|
|
51
|
-
|
|
52
|
-
type CommandRow = [signature: string, description: string]
|
|
53
|
-
|
|
54
|
-
// The grouping mirrors the README's domain table and has no other source, so a
|
|
55
|
-
// command added to the program needs a row here or help-option.test.ts fails.
|
|
56
|
-
const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
|
|
57
|
-
{
|
|
58
|
-
title: 'Project',
|
|
59
|
-
rows: [
|
|
60
|
-
['init [path]', 'Bootstrap a project with toolkit domains'],
|
|
61
|
-
['sync [path]', 'Sync all installed domains in a project'],
|
|
62
|
-
['upgrade', 'Reinstall the CLI with the manager that installed it'],
|
|
63
|
-
['migrate [cmd]', 'Move a project off a retired name or layout'],
|
|
64
|
-
['targets [cmd]', 'Projects this toolkit installed into (list, pulls)'],
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
title: 'Domains',
|
|
69
|
-
rows: [
|
|
70
|
-
['gov [cmd]', 'Governance rules (install, sync, list)'],
|
|
71
|
-
['standards [cmd]', 'Standards (install, sync, list, <name>)'],
|
|
72
|
-
['snippets [cmd]', 'Prompt snippets (create, list)'],
|
|
73
|
-
['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
|
|
74
|
-
['claude [cmd]', 'Claude workflow (init, sync, setup)'],
|
|
75
|
-
['wiki [cmd]', 'Wiki pages (init)'],
|
|
76
|
-
['indexes [cmd]', 'Regenerate index.md files (regen)'],
|
|
77
|
-
['docs [cmd|topic]', 'Emit toolkit reference docs (list, <topic>)'],
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
title: 'Author and render',
|
|
82
|
-
rows: [
|
|
83
|
-
['design [cmd]', 'Design system (render, board)'],
|
|
84
|
-
['slides [cmd]', 'Slide decks (render, list)'],
|
|
85
|
-
['capture [source]', 'Render HTML capture sources to PNG'],
|
|
86
|
-
['serve [dir]', 'Serve a directory on localhost, print the link'],
|
|
87
|
-
['demo [cmd]', 'Record a running app (compile, run)'],
|
|
88
|
-
['inventory [subj]', 'Report one computed property across every route'],
|
|
89
|
-
['drive <url> <run>', 'Walk a page through interactions, measure each'],
|
|
90
|
-
['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
|
|
91
|
-
['teach [cmd]', 'Learning workspaces (list, open, resource, glossary)'],
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
title: 'Session and ship',
|
|
96
|
-
rows: [
|
|
97
|
-
['sandbox [cat:cmd]', 'Provision and run sandbox scenarios'],
|
|
98
|
-
['tasks [cmd]', 'Task board (validate, archive)'],
|
|
99
|
-
['intake [cmd]', 'Intake folders under .canon/intake/ (list, answer)'],
|
|
100
|
-
['records [cmd]', 'Session records (validate, size, push, pull)'],
|
|
101
|
-
['sessions [cmd]', 'Resolve live sessions to worktree and branch'],
|
|
102
|
-
['worktrees [cmd]', 'Reclaim worktrees whose branches merged'],
|
|
103
|
-
['autoship [cmd]', 'Decide whether a changed set needs review'],
|
|
104
|
-
['pr [cmd]', 'Read a pull request (key-changes, head, checks)'],
|
|
105
|
-
['feedback', 'Write toolkit feedback from stdin'],
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
title: 'Repo reports',
|
|
110
|
-
rows: [
|
|
111
|
-
['audits [cmd]', 'Run every health check as one set (run, list)'],
|
|
112
|
-
['gate [cmd]', 'Run the merge gate stage by stage (run)'],
|
|
113
|
-
['secrets [cmd]', 'Scan the shipped tree for credentials (scan)'],
|
|
114
|
-
['deps [cmd]', 'Read the dependency set for advisories (audit)'],
|
|
115
|
-
['labels [cmd]', 'Read a changed set against the label map (audit)'],
|
|
116
|
-
['comments [cmd]', 'Measure comment density and trend (scan)'],
|
|
117
|
-
['context [cmd]', 'Report context folder health (audit)'],
|
|
118
|
-
['markdown [cmd]', 'Report markdown against the standards (audit)'],
|
|
119
|
-
['repo [cmd]', 'Remote metadata (metadata propose, apply)'],
|
|
120
|
-
['census [path]', 'Report file count, extensions, and line totals'],
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
]
|
|
124
|
-
|
|
125
|
-
// A row past the width is cut rather than wrapped, since a wrapped row breaks
|
|
126
|
-
// the alignment the grouping exists to buy.
|
|
127
|
-
function commandRow(signature: string, description: string): string {
|
|
128
|
-
const { GREY, NC } = palette(process.stdout)
|
|
129
|
-
const room = HELP_WIDTH - ROW_INDENT - SIGNATURE_WIDTH - '# '.length
|
|
130
|
-
const text =
|
|
131
|
-
description.length > room
|
|
132
|
-
? `${description.slice(0, room - 1)}…`
|
|
133
|
-
: description
|
|
134
|
-
return `${GREY}│${NC} ${signature.padEnd(SIGNATURE_WIDTH)}${GREY}# ${text}${NC}`
|
|
135
|
-
}
|
|
46
|
+
import { renderHelp } from '@/help'
|
|
136
47
|
|
|
137
48
|
function showHelp(): void {
|
|
138
|
-
|
|
139
|
-
// about that stream rather than the stderr every other writer here uses.
|
|
140
|
-
const { GREY, NC, WHITE } = palette(process.stdout)
|
|
141
|
-
const lines = [
|
|
142
|
-
`${GREY}┌${NC}`,
|
|
143
|
-
`${GREY}├${NC} ${WHITE}Usage:${NC} canon [command]`,
|
|
144
|
-
`${GREY}│${NC}`,
|
|
145
|
-
`${GREY}│${NC} ${WHITE}Commands:${NC}`,
|
|
146
|
-
...COMMAND_GROUPS.flatMap((group) => [
|
|
147
|
-
`${GREY}│${NC}`,
|
|
148
|
-
`${GREY}│${NC} ${WHITE}${group.title}${NC}`,
|
|
149
|
-
...group.rows.map(([signature, description]) =>
|
|
150
|
-
commandRow(signature, description),
|
|
151
|
-
),
|
|
152
|
-
]),
|
|
153
|
-
`${GREY}│${NC}`,
|
|
154
|
-
`${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
|
|
155
|
-
`${GREY}│${NC} canon sandbox ${GREY}# Interactive scenario picker${NC}`,
|
|
156
|
-
`${GREY}│${NC} canon sandbox git:commit ${GREY}# Run specific scenario${NC}`,
|
|
157
|
-
`${GREY}│${NC} canon sandbox reset ${GREY}# Reset sandbox to baseline${NC}`,
|
|
158
|
-
`${GREY}│${NC} canon sandbox clean ${GREY}# Wipe the sandbox${NC}`,
|
|
159
|
-
`${GREY}│${NC}`,
|
|
160
|
-
`${GREY}│${NC} ${WHITE}Examples:${NC}`,
|
|
161
|
-
`${GREY}│${NC} canon sync ../my-app`,
|
|
162
|
-
`${GREY}│${NC} canon sandbox git:commit`,
|
|
163
|
-
`${GREY}│${NC} canon gov install react`,
|
|
164
|
-
`${GREY}│${NC} canon gov sync ../my-app`,
|
|
165
|
-
`${GREY}│${NC} canon gov restated --json`,
|
|
166
|
-
`${GREY}│${NC} canon standards markdown`,
|
|
167
|
-
`${GREY}│${NC} canon snippets list`,
|
|
168
|
-
`${GREY}│${NC} canon init ../my-app`,
|
|
169
|
-
`${GREY}│${NC} canon tooling sync base`,
|
|
170
|
-
`${GREY}│${NC} canon tooling create`,
|
|
171
|
-
`${GREY}│${NC} canon claude init`,
|
|
172
|
-
`${GREY}│${NC} canon indexes regen`,
|
|
173
|
-
`${GREY}│${NC} canon indexes regen --dry-run --json`,
|
|
174
|
-
`${GREY}│${NC} canon docs list --json`,
|
|
175
|
-
`${GREY}│${NC} canon docs agents`,
|
|
176
|
-
`${GREY}│${NC} canon design render`,
|
|
177
|
-
`${GREY}│${NC} canon design board`,
|
|
178
|
-
`${GREY}│${NC} canon slides render`,
|
|
179
|
-
`${GREY}│${NC} canon slides list --json`,
|
|
180
|
-
`${GREY}│${NC} canon capture assets/captures/install.html --selector .window --out assets/evidence`,
|
|
181
|
-
`${GREY}│${NC} canon serve .canon/teach`,
|
|
182
|
-
`${GREY}│${NC} canon inventory focus --json`,
|
|
183
|
-
`${GREY}│${NC} canon drive http://localhost:4173 run.json --json`,
|
|
184
|
-
`${GREY}│${NC} pbpaste | canon feedback`,
|
|
185
|
-
`${GREY}│${NC} canon transcripts https://youtu.be/VIDEO_ID`,
|
|
186
|
-
`${GREY}│${NC} canon tasks archive --pull-request 673 --json`,
|
|
187
|
-
`${GREY}│${NC} canon intake list toolkit-overview --unread --json`,
|
|
188
|
-
`${GREY}│${NC} canon teach list --json`,
|
|
189
|
-
`${GREY}│${NC} canon comments scan src --json`,
|
|
190
|
-
`${GREY}│${NC} canon context audit --json`,
|
|
191
|
-
`${GREY}│${NC} canon markdown audit .claude/rules --json`,
|
|
192
|
-
`${GREY}│${NC} canon records validate plans`,
|
|
193
|
-
`${GREY}│${NC} canon records size --json`,
|
|
194
|
-
`${GREY}│${NC} canon records push --json`,
|
|
195
|
-
`${GREY}│${NC} canon sessions list --json`,
|
|
196
|
-
`${GREY}│${NC} canon worktrees list --json`,
|
|
197
|
-
`${GREY}│${NC} canon worktrees reclaim --dry-run`,
|
|
198
|
-
`${GREY}│${NC} canon secrets scan --json`,
|
|
199
|
-
`${GREY}│${NC} canon deps audit --json`,
|
|
200
|
-
`${GREY}│${NC} canon labels audit --json`,
|
|
201
|
-
`${GREY}│${NC} canon repo metadata propose --json`,
|
|
202
|
-
`${GREY}│${NC} canon census --json`,
|
|
203
|
-
`${GREY}│${NC} canon audits run --json`,
|
|
204
|
-
`${GREY}│${NC} canon gate run --all --no-write`,
|
|
205
|
-
`${GREY}│${NC} canon upgrade --json`,
|
|
206
|
-
`${GREY}└${NC}`,
|
|
207
|
-
]
|
|
208
|
-
console.log(lines.join('\n'))
|
|
49
|
+
console.log(renderHelp(process.stdout))
|
|
209
50
|
}
|
|
210
51
|
|
|
211
52
|
const program = new Command()
|
package/src/commands/pr.ts
CHANGED
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
compareKeyChanges,
|
|
21
21
|
treeRoots,
|
|
22
22
|
} from '@/pr/bijection'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type CheckRunListing,
|
|
25
|
+
collapseChecks,
|
|
26
|
+
withMergeState,
|
|
27
|
+
} from '@/pr/checks'
|
|
24
28
|
import {
|
|
25
29
|
findEvidenceChecklist,
|
|
26
30
|
findEvidenceCommentId,
|
|
@@ -307,6 +311,11 @@ export function register(program: Command): void {
|
|
|
307
311
|
'an empty row list, and reading that as passing is the false green the',
|
|
308
312
|
'sha key alone does not close.',
|
|
309
313
|
'',
|
|
314
|
+
'A branch conflicting with its base gets no merge ref, so no run ever',
|
|
315
|
+
'starts for it and the state stays pending. The record carries',
|
|
316
|
+
'`conflicted: true` beside it when `mergeStateStatus` reads DIRTY and no',
|
|
317
|
+
'run belongs to the tip, and a caller stops waiting and rebases.',
|
|
318
|
+
'',
|
|
310
319
|
'Exit codes:',
|
|
311
320
|
' 0 the runs for the tip were read, whatever they say',
|
|
312
321
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
@@ -865,6 +874,7 @@ interface PullIdentity {
|
|
|
865
874
|
readonly number: number | undefined
|
|
866
875
|
readonly branch: string
|
|
867
876
|
readonly head?: string
|
|
877
|
+
readonly mergeState?: string
|
|
868
878
|
}
|
|
869
879
|
|
|
870
880
|
type IdentityRead =
|
|
@@ -911,12 +921,17 @@ async function readIdentity(
|
|
|
911
921
|
|
|
912
922
|
const args = ['pr', 'view']
|
|
913
923
|
if (number !== undefined) args.push(number)
|
|
914
|
-
args.push('--json', 'number,headRefName,headRefOid')
|
|
924
|
+
args.push('--json', 'number,headRefName,headRefOid,mergeStateStatus')
|
|
915
925
|
|
|
916
926
|
const stdout = await gh(cwd, args)
|
|
917
927
|
if (stdout === null) return { kind: 'refused', reason: 'gh-failed' }
|
|
918
928
|
|
|
919
|
-
let row: {
|
|
929
|
+
let row: {
|
|
930
|
+
number?: number
|
|
931
|
+
headRefName?: string
|
|
932
|
+
headRefOid?: string
|
|
933
|
+
mergeStateStatus?: string
|
|
934
|
+
}
|
|
920
935
|
try {
|
|
921
936
|
row = JSON.parse(stdout)
|
|
922
937
|
} catch {
|
|
@@ -933,6 +948,9 @@ async function readIdentity(
|
|
|
933
948
|
number: row.number,
|
|
934
949
|
branch: row.headRefName,
|
|
935
950
|
...(row.headRefOid !== undefined && { head: row.headRefOid }),
|
|
951
|
+
...(row.mergeStateStatus !== undefined && {
|
|
952
|
+
mergeState: row.mergeStateStatus,
|
|
953
|
+
}),
|
|
936
954
|
},
|
|
937
955
|
}
|
|
938
956
|
}
|
|
@@ -1071,7 +1089,10 @@ async function runChecks(
|
|
|
1071
1089
|
)
|
|
1072
1090
|
}
|
|
1073
1091
|
|
|
1074
|
-
const reading =
|
|
1092
|
+
const reading = withMergeState(
|
|
1093
|
+
identity.mergeState,
|
|
1094
|
+
collapseChecks(resolved.tip, listing),
|
|
1095
|
+
)
|
|
1075
1096
|
|
|
1076
1097
|
logStep('Scope')
|
|
1077
1098
|
logInfo(
|
|
@@ -1101,6 +1122,10 @@ async function runChecks(
|
|
|
1101
1122
|
logInfo('every run on the tip completed and none failed')
|
|
1102
1123
|
} else if (reading.state === 'failing') {
|
|
1103
1124
|
logWarn('a run on the tip failed, which no run still going can clear')
|
|
1125
|
+
} else if (reading.conflicted) {
|
|
1126
|
+
logWarn(
|
|
1127
|
+
'the branch conflicts with its base, so no run will start for the tip. Rebase onto the base rather than waiting.',
|
|
1128
|
+
)
|
|
1104
1129
|
} else if (reading.matched === 0) {
|
|
1105
1130
|
logInfo(
|
|
1106
1131
|
`no run belongs to the tip yet${reading.reported > 0 ? `, against a reported count of ${reading.reported}` : ''}. That is unread rather than clean.`,
|
package/src/commands/tasks.ts
CHANGED
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
type PlanCitations,
|
|
14
14
|
planCitations,
|
|
15
15
|
} from '@/tasks/archive'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type LabelOutcome,
|
|
18
|
+
type LabelRefused,
|
|
19
|
+
claimLabel,
|
|
20
|
+
nextLabel,
|
|
21
|
+
} from '@/tasks/label'
|
|
17
22
|
import { listTasks } from '@/tasks/list'
|
|
18
23
|
import {
|
|
19
24
|
type Claim,
|
|
@@ -122,6 +127,7 @@ interface ListCommandOptions {
|
|
|
122
127
|
|
|
123
128
|
interface NextLabelCommandOptions {
|
|
124
129
|
readonly json?: boolean
|
|
130
|
+
readonly claim?: boolean
|
|
125
131
|
readonly root?: string
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -503,6 +509,7 @@ export function register(program: Command): void {
|
|
|
503
509
|
)
|
|
504
510
|
.helpOption('-h, --help', 'Show this help message')
|
|
505
511
|
.option('--json', 'Emit a machine-readable record on stdout')
|
|
512
|
+
.option('--claim', 'Reserve the label so a second claim gets another')
|
|
506
513
|
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
507
514
|
.addHelpText(
|
|
508
515
|
'after',
|
|
@@ -515,14 +522,18 @@ export function register(program: Command): void {
|
|
|
515
522
|
'Exit codes:',
|
|
516
523
|
' 0 the label is derived',
|
|
517
524
|
' 1 refused with no-board',
|
|
525
|
+
' 2 refused with label-contended, on --claim only',
|
|
518
526
|
'',
|
|
519
|
-
'
|
|
520
|
-
'second can
|
|
521
|
-
'
|
|
527
|
+
'Bare, it reports and never writes, so two sessions calling it in the',
|
|
528
|
+
'same second can take the same answer. --claim reserves the label',
|
|
529
|
+
'under .canon/ordinal-locks/ and counts every reservation in the',
|
|
530
|
+
'scan, so two claims return different labels. A dead claim leaves a',
|
|
531
|
+
'gap, which standards/versioning.md permits.',
|
|
522
532
|
'',
|
|
523
533
|
'Examples:',
|
|
524
534
|
' canon tasks next-label',
|
|
525
535
|
' canon tasks next-label --json',
|
|
536
|
+
' canon tasks next-label --claim --json',
|
|
526
537
|
'',
|
|
527
538
|
].join('\n'),
|
|
528
539
|
)
|
|
@@ -864,11 +875,15 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
864
875
|
|
|
865
876
|
async function runNextLabel(opts: NextLabelCommandOptions): Promise<number> {
|
|
866
877
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
867
|
-
const outcome = await nextLabel(root)
|
|
878
|
+
const outcome = opts.claim ? await claimLabel(root) : await nextLabel(root)
|
|
868
879
|
|
|
869
880
|
return reportNextLabel(outcome, opts.json ?? false, root)
|
|
870
881
|
}
|
|
871
882
|
|
|
883
|
+
function refusalExit(outcome: LabelRefused): number {
|
|
884
|
+
return outcome.reason === 'label-contended' ? 2 : 1
|
|
885
|
+
}
|
|
886
|
+
|
|
872
887
|
function reportNextLabel(
|
|
873
888
|
outcome: LabelOutcome,
|
|
874
889
|
emitJson: boolean,
|
|
@@ -879,14 +894,14 @@ function reportNextLabel(
|
|
|
879
894
|
process.stdout.write(
|
|
880
895
|
`${JSON.stringify({ ok: false, reason: outcome.reason, message: outcome.message })}\n`,
|
|
881
896
|
)
|
|
882
|
-
return
|
|
897
|
+
return refusalExit(outcome)
|
|
883
898
|
}
|
|
884
899
|
|
|
885
900
|
intro('canon tasks next-label')
|
|
886
901
|
logStep('Refused')
|
|
887
902
|
logError(outcome.message)
|
|
888
903
|
outro()
|
|
889
|
-
return
|
|
904
|
+
return refusalExit(outcome)
|
|
890
905
|
}
|
|
891
906
|
|
|
892
907
|
if (emitJson) {
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { palette } from '@/ui'
|
|
2
|
+
|
|
3
|
+
const HELP_WIDTH = 80
|
|
4
|
+
const SIGNATURE_WIDTH = 19
|
|
5
|
+
const SANDBOX_SIGNATURE_WIDTH = 26
|
|
6
|
+
const ROW_INDENT = '│ '.length
|
|
7
|
+
|
|
8
|
+
type CommandRow = [signature: string, description: string]
|
|
9
|
+
type HelpStream = { isTTY?: boolean }
|
|
10
|
+
|
|
11
|
+
// The grouping mirrors the README's domain table and has no other source, so a
|
|
12
|
+
// command added to the program needs a row here or help-option.test.ts fails.
|
|
13
|
+
export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
|
|
14
|
+
{
|
|
15
|
+
title: 'Project',
|
|
16
|
+
rows: [
|
|
17
|
+
['init [path]', 'Bootstrap a project with toolkit domains'],
|
|
18
|
+
['sync [path]', 'Sync all installed domains in a project'],
|
|
19
|
+
['upgrade', 'Reinstall the CLI with the manager that installed it'],
|
|
20
|
+
['migrate [cmd]', 'Move a project off a retired name or layout'],
|
|
21
|
+
['targets [cmd]', 'Projects this toolkit installed into (list, pulls)'],
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: 'Domains',
|
|
26
|
+
rows: [
|
|
27
|
+
['gov [cmd]', 'Governance rules (install, sync, list)'],
|
|
28
|
+
['standards [cmd]', 'Standards (install, sync, list, <name>)'],
|
|
29
|
+
['snippets [cmd]', 'Prompt snippets (create, list)'],
|
|
30
|
+
['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
|
|
31
|
+
['claude [cmd]', 'Claude workflow (init, sync, setup)'],
|
|
32
|
+
['wiki [cmd]', 'Wiki pages (init)'],
|
|
33
|
+
['indexes [cmd]', 'Regenerate index.md files (regen)'],
|
|
34
|
+
['docs [cmd|topic]', 'Emit toolkit reference docs (list, <topic>)'],
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Author and render',
|
|
39
|
+
rows: [
|
|
40
|
+
['design [cmd]', 'Design system (render, board)'],
|
|
41
|
+
['slides [cmd]', 'Slide decks (render, list)'],
|
|
42
|
+
['capture [source]', 'Render HTML capture sources to PNG'],
|
|
43
|
+
['serve [dir]', 'Serve a directory on localhost, print the link'],
|
|
44
|
+
['demo [cmd]', 'Record a running app (compile, run)'],
|
|
45
|
+
['inventory [subj]', 'Report one computed property across every route'],
|
|
46
|
+
['drive <url> <run>', 'Walk a page through interactions, measure each'],
|
|
47
|
+
['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
|
|
48
|
+
['teach [cmd]', 'Learning workspaces (list, open, resource, glossary)'],
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: 'Session and ship',
|
|
53
|
+
rows: [
|
|
54
|
+
['sandbox [cat:cmd]', 'Provision and run sandbox scenarios'],
|
|
55
|
+
['tasks [cmd]', 'Task board (validate, archive)'],
|
|
56
|
+
['intake [cmd]', 'Intake folders under .canon/intake/ (list, answer)'],
|
|
57
|
+
['records [cmd]', 'Session records (validate, size, push, pull)'],
|
|
58
|
+
['sessions [cmd]', 'Resolve live sessions to worktree and branch'],
|
|
59
|
+
['worktrees [cmd]', 'Reclaim worktrees whose branches merged'],
|
|
60
|
+
['autoship [cmd]', 'Decide whether a changed set needs review'],
|
|
61
|
+
['pr [cmd]', 'Read a pull request (key-changes, head, checks)'],
|
|
62
|
+
['feedback', 'Write toolkit feedback from stdin'],
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'Repo reports',
|
|
67
|
+
rows: [
|
|
68
|
+
['audits [cmd]', 'Run every health check as one set (run, list)'],
|
|
69
|
+
['gate [cmd]', 'Run the merge gate stage by stage (run)'],
|
|
70
|
+
['secrets [cmd]', 'Scan the shipped tree for credentials (scan)'],
|
|
71
|
+
['deps [cmd]', 'Read the dependency set for advisories (audit)'],
|
|
72
|
+
['labels [cmd]', 'Read a changed set against the label map (audit)'],
|
|
73
|
+
['comments [cmd]', 'Measure comment density and trend (scan)'],
|
|
74
|
+
['context [cmd]', 'Report context folder health (audit)'],
|
|
75
|
+
['markdown [cmd]', 'Report markdown against the standards (audit)'],
|
|
76
|
+
['repo [cmd]', 'Remote metadata (metadata propose, apply)'],
|
|
77
|
+
['census [path]', 'Report file count, extensions, and line totals'],
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
const SANDBOX_ROWS: CommandRow[] = [
|
|
83
|
+
['canon sandbox', 'Interactive scenario picker'],
|
|
84
|
+
['canon sandbox git:commit', 'Run specific scenario'],
|
|
85
|
+
['canon sandbox reset', 'Reset sandbox to baseline'],
|
|
86
|
+
['canon sandbox clean', 'Wipe the sandbox'],
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
const EXAMPLES = [
|
|
90
|
+
'canon sync ../my-app',
|
|
91
|
+
'canon sandbox git:commit',
|
|
92
|
+
'canon gov install react',
|
|
93
|
+
'canon gov sync ../my-app',
|
|
94
|
+
'canon gov restated --json',
|
|
95
|
+
'canon standards markdown',
|
|
96
|
+
'canon snippets list',
|
|
97
|
+
'canon init ../my-app',
|
|
98
|
+
'canon tooling sync base',
|
|
99
|
+
'canon tooling create',
|
|
100
|
+
'canon claude init',
|
|
101
|
+
'canon indexes regen',
|
|
102
|
+
'canon indexes regen --dry-run --json',
|
|
103
|
+
'canon docs list --json',
|
|
104
|
+
'canon docs agents',
|
|
105
|
+
'canon design render',
|
|
106
|
+
'canon design board',
|
|
107
|
+
'canon slides render',
|
|
108
|
+
'canon slides list --json',
|
|
109
|
+
'canon capture assets/captures/install.html --selector .window --out assets/evidence',
|
|
110
|
+
'canon serve .canon/teach',
|
|
111
|
+
'canon inventory focus --json',
|
|
112
|
+
'canon drive http://localhost:4173 run.json --json',
|
|
113
|
+
'pbpaste | canon feedback',
|
|
114
|
+
'canon transcripts https://youtu.be/VIDEO_ID',
|
|
115
|
+
'canon tasks archive --pull-request 673 --json',
|
|
116
|
+
'canon intake list toolkit-overview --unread --json',
|
|
117
|
+
'canon teach list --json',
|
|
118
|
+
'canon comments scan src --json',
|
|
119
|
+
'canon context audit --json',
|
|
120
|
+
'canon markdown audit .claude/rules --json',
|
|
121
|
+
'canon records validate plans',
|
|
122
|
+
'canon records size --json',
|
|
123
|
+
'canon records push --json',
|
|
124
|
+
'canon sessions list --json',
|
|
125
|
+
'canon worktrees list --json',
|
|
126
|
+
'canon worktrees reclaim --dry-run',
|
|
127
|
+
'canon secrets scan --json',
|
|
128
|
+
'canon deps audit --json',
|
|
129
|
+
'canon labels audit --json',
|
|
130
|
+
'canon repo metadata propose --json',
|
|
131
|
+
'canon census --json',
|
|
132
|
+
'canon audits run --json',
|
|
133
|
+
'canon gate run --all --no-write',
|
|
134
|
+
'canon upgrade --json',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
// The frame follows stdout's TTY state rather than `palette`, whose blank
|
|
138
|
+
// palette keeps every glyph so the stderr frames a captured run quotes stay
|
|
139
|
+
// framed. A glyph is rendering rather than color, so NO_COLOR keeps it.
|
|
140
|
+
function isFramed(stream: HelpStream): boolean {
|
|
141
|
+
return stream.isTTY === true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function gutter(stream: HelpStream): string {
|
|
145
|
+
if (!isFramed(stream)) return ''
|
|
146
|
+
const { GREY, NC } = palette(stream)
|
|
147
|
+
return `${GREY}│${NC}`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A row past the width is cut rather than wrapped, since a wrapped row breaks
|
|
151
|
+
// the alignment the grouping exists to buy. The cut is computed from the framed
|
|
152
|
+
// prefix in both forms, so a pipe carries the same text as a terminal.
|
|
153
|
+
export function commandRow(
|
|
154
|
+
signature: string,
|
|
155
|
+
description: string,
|
|
156
|
+
stream: HelpStream,
|
|
157
|
+
): string {
|
|
158
|
+
const { GREY, NC } = palette(stream)
|
|
159
|
+
const room = HELP_WIDTH - ROW_INDENT - SIGNATURE_WIDTH - '# '.length
|
|
160
|
+
const text =
|
|
161
|
+
description.length > room
|
|
162
|
+
? `${description.slice(0, room - 1)}…`
|
|
163
|
+
: description
|
|
164
|
+
return `${gutter(stream)} ${signature.padEnd(SIGNATURE_WIDTH)}${GREY}# ${text}${NC}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function renderHelp(stream: HelpStream): string {
|
|
168
|
+
const { GREY, NC, WHITE } = palette(stream)
|
|
169
|
+
const bar = gutter(stream)
|
|
170
|
+
const usage = `${WHITE}Usage:${NC} canon [command]`
|
|
171
|
+
const lines = [
|
|
172
|
+
...(isFramed(stream)
|
|
173
|
+
? [`${GREY}┌${NC}`, `${GREY}├${NC} ${usage}`]
|
|
174
|
+
: [usage]),
|
|
175
|
+
bar,
|
|
176
|
+
`${bar} ${WHITE}Commands:${NC}`,
|
|
177
|
+
...COMMAND_GROUPS.flatMap((group) => [
|
|
178
|
+
bar,
|
|
179
|
+
`${bar} ${WHITE}${group.title}${NC}`,
|
|
180
|
+
...group.rows.map(([signature, description]) =>
|
|
181
|
+
commandRow(signature, description, stream),
|
|
182
|
+
),
|
|
183
|
+
]),
|
|
184
|
+
bar,
|
|
185
|
+
`${bar} ${WHITE}Sandbox:${NC}`,
|
|
186
|
+
...SANDBOX_ROWS.map(
|
|
187
|
+
([signature, description]) =>
|
|
188
|
+
`${bar} ${signature.padEnd(SANDBOX_SIGNATURE_WIDTH)}${GREY}# ${description}${NC}`,
|
|
189
|
+
),
|
|
190
|
+
bar,
|
|
191
|
+
`${bar} ${WHITE}Examples:${NC}`,
|
|
192
|
+
...EXAMPLES.map((example) => `${bar} ${example}`),
|
|
193
|
+
...(isFramed(stream) ? [`${GREY}└${NC}`] : []),
|
|
194
|
+
]
|
|
195
|
+
return lines.join('\n')
|
|
196
|
+
}
|
package/src/pr/checks.ts
CHANGED
|
@@ -29,6 +29,35 @@ export interface ChecksReading {
|
|
|
29
29
|
readonly collapsed: number
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface ConflictedReading extends ChecksReading {
|
|
33
|
+
/** The branch conflicts with its base, so no run will ever start for the tip. */
|
|
34
|
+
readonly conflicted: boolean
|
|
35
|
+
/** The pull request's `mergeStateStatus`, absent when the read carried none. */
|
|
36
|
+
readonly mergeState?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tells a conflicted pull request from a tip whose runs have not started.
|
|
41
|
+
*
|
|
42
|
+
* A branch that conflicts with its base gets no merge ref, so no
|
|
43
|
+
* `pull_request` workflow runs and the listing stays empty, which
|
|
44
|
+
* `collapseChecks` can only call pending. Only `DIRTY` separates the two, and
|
|
45
|
+
* only while no run belongs to the tip: a conflict arising after runs
|
|
46
|
+
* completed leaves those runs standing as a real answer. `UNKNOWN` is
|
|
47
|
+
* transient while GitHub computes it, so it reads as not conflicted and costs
|
|
48
|
+
* a caller one more poll.
|
|
49
|
+
*/
|
|
50
|
+
export function withMergeState(
|
|
51
|
+
mergeState: string | undefined,
|
|
52
|
+
reading: ChecksReading,
|
|
53
|
+
): ConflictedReading {
|
|
54
|
+
return {
|
|
55
|
+
...reading,
|
|
56
|
+
conflicted: mergeState === 'DIRTY' && reading.matched === 0,
|
|
57
|
+
...(mergeState !== undefined && { mergeState }),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
/**
|
|
33
62
|
* Puts a check-run row into the shape `rollup` reads.
|
|
34
63
|
*
|