@erclx/canon 5.32.0 → 5.34.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 +1 -2
- package/claude/skills/api-design/SKILL.md +2 -3
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/auto-ship/references/worktree-name.md +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
- package/claude/skills/canon-cli/SKILL.md +75 -32
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
- package/claude/skills/canon-feedback/SKILL.md +11 -29
- package/claude/skills/canon-feedback/references/template.md +53 -0
- package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
- package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
- package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
- package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +0 -1
- package/claude/skills/context-fold/REQUIREMENT.md +25 -33
- package/claude/skills/context-fold/SKILL.md +25 -70
- package/claude/skills/context-fold/references/classify.md +3 -3
- package/claude/skills/context-fold/references/completion-report.md +4 -4
- package/claude/skills/context-fold/references/context-refresh.md +5 -5
- package/{standards → claude/skills/context-fold/references}/context.md +16 -5
- package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
- package/claude/skills/create-rule/SKILL.md +3 -3
- package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
- package/claude/skills/design-taste/REQUIREMENT.md +3 -3
- package/claude/skills/design-taste/SKILL.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +3 -3
- package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
- package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
- package/claude/skills/draft-and-pick/SKILL.md +5 -5
- package/claude/skills/draft-doc/SKILL.md +6 -6
- package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
- package/claude/skills/draft-figure/SKILL.md +2 -2
- package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
- package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
- package/claude/skills/draft-identity/SKILL.md +2 -2
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
- package/claude/skills/git-commit/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +4 -1
- package/claude/skills/git-pr/SKILL.md +13 -13
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +38 -14
- package/claude/skills/git-pr/references/labels.md +5 -1
- package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
- package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
- package/claude/skills/git-pr/references/task-number.md +1 -1
- package/claude/skills/git-ship/SKILL.md +15 -14
- package/claude/skills/git-split/SKILL.md +3 -3
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/body-variants.md +2 -2
- package/claude/skills/review-pr/references/key-changes.md +3 -3
- package/claude/skills/review-pr/references/post.md +4 -2
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +8 -6
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +53 -17
- package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
- package/claude/skills/role-planner/REQUIREMENT.md +2 -1
- package/claude/skills/role-planner/SKILL.md +13 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -0
- package/claude/skills/role-worker/SKILL.md +9 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-worktree/SKILL.md +5 -5
- package/claude/skills/target-setup/SKILL.md +10 -4
- package/claude/skills/target-setup/references/detect.md +20 -3
- package/claude/skills/task-board/REQUIREMENT.md +7 -12
- package/claude/skills/task-board/SKILL.md +5 -17
- package/claude/skills/task-board/references/tasks.md +2 -2
- package/claude/skills/task-complete/REQUIREMENT.md +47 -0
- package/claude/skills/task-complete/SKILL.md +86 -0
- package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
- package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
- package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
- package/claude/skills/teach-workspace/SKILL.md +8 -4
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
- package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
- package/claude/skills/teach-workspace/references/promotion.md +3 -3
- package/claude/skills/test-craft/REQUIREMENT.md +2 -3
- package/claude/skills/test-craft/SKILL.md +15 -3
- 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/docs/agents/canvas.md +6 -2
- package/docs/agents/commands.md +4 -3
- package/docs/agents/context-audit-checks.md +4 -4
- package/docs/agents/gov-detect.md +47 -0
- package/docs/agents/index.md +7 -4
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +10 -11
- package/docs/agents/key-changes.md +5 -5
- package/docs/agents/label-coverage.md +11 -0
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/pr-evidence-layout.md +70 -0
- package/docs/agents/pr-evidence.md +8 -9
- package/docs/agents/pr-frames.md +7 -2
- package/docs/agents/pr-lift.md +132 -0
- package/docs/agents/records-push.md +2 -2
- package/docs/agents/records-stale.md +12 -47
- package/docs/agents/records-validate.md +3 -7
- package/docs/agents/records.md +5 -27
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +4 -4
- package/docs/agents/scripting.md +2 -1
- package/docs/agents/skills-audit.md +1 -1
- package/docs/agents/tasks-plans.md +2 -2
- package/docs/agents/tasks.md +2 -2
- package/docs/target/migrations.md +10 -2
- package/docs/target/projects.md +4 -4
- package/docs/target/sync.md +1 -1
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/parallel-features.md +7 -7
- package/docs/workflow/skill-map-on-request.md +9 -9
- package/docs/workflow/skill-map.md +12 -11
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +201 -0
- package/governance/renames.toml +0 -11
- package/governance/rules/canon/context.md +13 -0
- package/governance/rules/claude/skill.md +17 -0
- package/governance/rules/code/000-code.md +1 -13
- package/governance/rules/code/010-testing.md +2 -30
- package/governance/rules/ui/400-ui.md +1 -1
- package/governance/rules/ui/480-canvas.md +13 -0
- package/governance/stacks/base.toml +2 -4
- package/package.json +4 -4
- package/scripts/core/install-check.sh +3 -3
- package/scripts/core/regen-hero.ts +0 -6
- package/scripts/lib/ui.sh +0 -168
- package/src/audits/catalog.ts +0 -1
- package/src/canvas/arrange.ts +66 -0
- package/src/canvas/capture.ts +20 -6
- package/src/canvas/content.ts +39 -0
- package/src/canvas/fit.ts +238 -0
- package/src/canvas/tokens.ts +28 -10
- package/src/capture/frame-values.ts +2 -32
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +4 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +2 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/ui.ts +2 -3
- package/src/commands/canvas.ts +179 -1
- package/src/commands/claude.ts +1 -1
- package/src/commands/gov.ts +146 -16
- package/src/commands/labels.ts +326 -7
- package/src/commands/migrate.ts +6 -1
- package/src/commands/pr.ts +377 -2
- package/src/commands/records.ts +13 -399
- package/src/commands/sandbox.ts +3 -6
- package/src/commands/tasks.ts +3 -3
- package/src/context/audit.ts +5 -5
- package/src/context/folders.ts +1 -1
- package/src/design/budget.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/regen.ts +1 -1
- package/src/design/render.ts +1 -1
- package/src/feedback/body.ts +26 -5
- package/src/gate/measures.ts +59 -45
- package/src/gate/sequencer.ts +56 -1
- package/src/gate/stages.ts +4 -11
- package/src/gov/detect.ts +301 -0
- package/src/gov/install.ts +2 -3
- package/src/gov/list.ts +23 -2
- package/src/gov/stacks.ts +10 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/labels/apply.ts +39 -0
- package/src/labels/format.ts +3 -3
- package/src/labels/phase.ts +3 -3
- package/src/labels/sections.ts +43 -0
- package/src/migrate/skill-names.ts +20 -1
- package/src/pr/evidence.ts +218 -45
- package/src/pr/frames.ts +23 -3
- package/src/pr/lift.ts +418 -0
- package/src/pr/paths.ts +4 -4
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +76 -0
- package/src/records/backup.ts +16 -6
- package/src/records/stale-canonical.ts +31 -1
- package/src/records/validate.ts +2 -188
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +2 -6
- package/src/sandbox/equivalence.ts +19 -51
- package/src/sandbox/expect.ts +4 -4
- package/src/sandbox/provision.ts +13 -36
- package/src/sandbox/scenario.ts +26 -48
- package/src/sandbox/tree.ts +2 -6
- package/src/standards/read.ts +14 -1
- package/src/sync/git.ts +1 -1
- package/src/tasks/archive.ts +5 -5
- package/src/tasks/branch.ts +3 -3
- package/src/tasks/label.ts +1 -1
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/standards/index.md +0 -13
- package/standards/standard.md +1 -7
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
- package/tooling/claude/seeds/.claude/settings.json +9 -0
- package/governance/rules/standards/context.md +0 -14
- package/governance/rules/standards/design.md +0 -12
- package/governance/rules/standards/readme.md +0 -12
- package/governance/rules/standards/rule.md +0 -13
- package/governance/rules/standards/skill.md +0 -21
- package/governance/rules/standards/standard.md +0 -13
- package/scripts/config.sh +0 -11
- package/scripts/lib/gov.sh +0 -14
- package/scripts/lib/sandbox-fixtures.sh +0 -191
- package/scripts/lib/sandbox-git.sh +0 -125
- package/scripts/lib/sandbox-path.sh +0 -50
- package/scripts/sandbox-hook.sh +0 -99
- package/src/gov/standard-rules.ts +0 -179
- package/src/records/migrate.ts +0 -78
- package/src/records/stale.ts +0 -243
- package/src/sandbox/hooks.ts +0 -128
- /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
- /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
package/src/commands/pr.ts
CHANGED
|
@@ -48,15 +48,27 @@ import {
|
|
|
48
48
|
FRAMES_BRANCH,
|
|
49
49
|
type FramesApi,
|
|
50
50
|
type FramesRefusal,
|
|
51
|
+
type FrameTheme,
|
|
51
52
|
frameLink,
|
|
52
53
|
isFrameHead,
|
|
53
54
|
isFramePass,
|
|
55
|
+
isFrameTheme,
|
|
54
56
|
pruneFrames,
|
|
55
57
|
pushFrame,
|
|
56
58
|
readFrame,
|
|
57
59
|
type TreeEntry,
|
|
58
60
|
} from '@/pr/frames'
|
|
59
61
|
import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
|
|
62
|
+
import {
|
|
63
|
+
DEFAULT_WAIT_SECONDS,
|
|
64
|
+
type EvidenceGate,
|
|
65
|
+
type GitRunner,
|
|
66
|
+
lift,
|
|
67
|
+
type LiftApi,
|
|
68
|
+
type LiftRefusal,
|
|
69
|
+
type PullState,
|
|
70
|
+
readBaseStanding,
|
|
71
|
+
} from '@/pr/lift'
|
|
60
72
|
import {
|
|
61
73
|
dropAddressLine,
|
|
62
74
|
findLocalServer,
|
|
@@ -82,6 +94,7 @@ import { type TickRefusal, tickBoxes } from '@/pr/tick'
|
|
|
82
94
|
import {
|
|
83
95
|
commentRowOf,
|
|
84
96
|
identityOf,
|
|
97
|
+
mergeStateOf,
|
|
85
98
|
type PullIdentity,
|
|
86
99
|
parseJsonLines,
|
|
87
100
|
type RestComment,
|
|
@@ -91,6 +104,7 @@ import {
|
|
|
91
104
|
selectBranchPull,
|
|
92
105
|
} from '@/pr/rest'
|
|
93
106
|
import { resolveReviewScope } from '@/pr/review-scope'
|
|
107
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
94
108
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
|
|
95
109
|
|
|
96
110
|
const GH_TIMEOUT_MS = 30_000
|
|
@@ -117,6 +131,11 @@ interface EvidenceOptions extends ReadOptions {
|
|
|
117
131
|
readonly check?: boolean
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
interface LiftCommandOptions extends ReadOptions {
|
|
135
|
+
readonly check?: boolean
|
|
136
|
+
readonly wait?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
interface LocalOptions extends ReadOptions {
|
|
121
140
|
readonly remove?: boolean
|
|
122
141
|
}
|
|
@@ -164,6 +183,7 @@ interface FramesOptions extends ReadOptions {
|
|
|
164
183
|
readonly box?: string
|
|
165
184
|
readonly head?: string
|
|
166
185
|
readonly pass?: string
|
|
186
|
+
readonly theme?: string
|
|
167
187
|
readonly drop?: boolean
|
|
168
188
|
readonly prune?: string
|
|
169
189
|
}
|
|
@@ -175,6 +195,7 @@ type FramesCommandRefusal =
|
|
|
175
195
|
| 'bad-box'
|
|
176
196
|
| 'bad-head'
|
|
177
197
|
| 'bad-pass'
|
|
198
|
+
| 'bad-theme'
|
|
178
199
|
| 'bad-days'
|
|
179
200
|
| 'unreadable-frame'
|
|
180
201
|
| 'read-only'
|
|
@@ -193,6 +214,8 @@ const FRAMES_REFUSALS: Record<FramesCommandRefusal, string> = {
|
|
|
193
214
|
'--head takes a commit sha, 7 to 40 lowercase hex characters. A branch name or a path would file the frame under a folder that names no commit.',
|
|
194
215
|
'bad-pass':
|
|
195
216
|
'--add takes --pass <stamp>, the instant the pass started in compact UTC such as 20261002T154450Z, so a second pass never replaces an earlier one.',
|
|
217
|
+
'bad-theme':
|
|
218
|
+
'--theme takes dark or light and goes with --add, so a second shot of one box never replaces the first.',
|
|
196
219
|
'bad-days': '--prune takes a positive whole number of days.',
|
|
197
220
|
'unreadable-frame':
|
|
198
221
|
'The file named by --add is missing, empty, or not a PNG, so it would embed as a broken image. Nothing is pushed.',
|
|
@@ -389,7 +412,7 @@ export function register(program: Command): void {
|
|
|
389
412
|
' unresolved a path written partially, or one past its first comma,',
|
|
390
413
|
' which can credit a changed file and never accuse one',
|
|
391
414
|
'',
|
|
392
|
-
`Only ## ${KEY_CHANGES} is read. ## Technical
|
|
415
|
+
`Only ## ${KEY_CHANGES} is read. ## Technical context legitimately names`,
|
|
393
416
|
'files a branch never touched, so widening the read manufactures findings.',
|
|
394
417
|
'',
|
|
395
418
|
'One class survives the reader: a bullet citing where something is defined',
|
|
@@ -650,6 +673,62 @@ export function register(program: Command): void {
|
|
|
650
673
|
process.exitCode = await runEvidence(number, opts)
|
|
651
674
|
})
|
|
652
675
|
|
|
676
|
+
pr.command('lift')
|
|
677
|
+
.description(
|
|
678
|
+
"Lift a pull request's draft mark once its reviews, evidence, standing, and CI all read clear at the head",
|
|
679
|
+
)
|
|
680
|
+
.argument('[number]', 'Pull request to lift, defaulting to this branch')
|
|
681
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
682
|
+
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
683
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
684
|
+
.option('--check', 'Report every gate and write nothing')
|
|
685
|
+
.option(
|
|
686
|
+
'--wait <seconds>',
|
|
687
|
+
`Hold for a pending CI run up to this long, default ${DEFAULT_WAIT_SECONDS}`,
|
|
688
|
+
)
|
|
689
|
+
.addHelpText(
|
|
690
|
+
'after',
|
|
691
|
+
[
|
|
692
|
+
'',
|
|
693
|
+
'The verb reads the gate itself, so any session that closes a review can',
|
|
694
|
+
'run it and nobody has to vouch for who is asking. Gates, in the order',
|
|
695
|
+
'they are read:',
|
|
696
|
+
' review the newest `## Review` is closed at the head, off its marker',
|
|
697
|
+
' ui the newest `## UI review` is closed at the head, bound only',
|
|
698
|
+
' where `canon pr evidence --check` carries a checklist',
|
|
699
|
+
' evidence the evidence check reads settled',
|
|
700
|
+
' standing the head is fresh against the base, not stale or conflicted',
|
|
701
|
+
' ci every check run on the tip passed, waited on up to --wait',
|
|
702
|
+
'',
|
|
703
|
+
'Review state, UI state, evidence, and standing are read before the CI',
|
|
704
|
+
'wait, so a reviewer whose partner is still driving refuses at once. The',
|
|
705
|
+
'head and the reviews are read again before the mutation.',
|
|
706
|
+
'',
|
|
707
|
+
'Reasons:',
|
|
708
|
+
' lifted, already-ready, clear (--check)',
|
|
709
|
+
' pull-closed, review-missing, review-open, review-unmarked,',
|
|
710
|
+
' review-behind, ui-missing, ui-open, ui-unmarked, ui-behind,',
|
|
711
|
+
' evidence-owed, standing-stale, standing-conflict, standing-unknown,',
|
|
712
|
+
' ci-pending (--check), ci-failing, ci-conflicted, ci-timeout,',
|
|
713
|
+
' head-moved, gh-failed',
|
|
714
|
+
'',
|
|
715
|
+
'Every read is REST. The mutation is the one GraphQL call.',
|
|
716
|
+
'',
|
|
717
|
+
'Exit codes:',
|
|
718
|
+
' 0 lifted, already-ready, or every gate clear under --check',
|
|
719
|
+
' 1 refused, with the reason in the JSON record',
|
|
720
|
+
'',
|
|
721
|
+
'Examples:',
|
|
722
|
+
' canon pr lift 1341 --json',
|
|
723
|
+
' canon pr lift 1341 --check --json',
|
|
724
|
+
' canon pr lift --wait 600',
|
|
725
|
+
'',
|
|
726
|
+
].join('\n'),
|
|
727
|
+
)
|
|
728
|
+
.action(async (number: string | undefined, opts: LiftCommandOptions) => {
|
|
729
|
+
process.exitCode = await runLift(number, opts)
|
|
730
|
+
})
|
|
731
|
+
|
|
653
732
|
pr.command('preview')
|
|
654
733
|
.description(
|
|
655
734
|
"Deploy the pull request's branch to a Cloudflare Pages preview and report its address",
|
|
@@ -841,6 +920,10 @@ export function register(program: Command): void {
|
|
|
841
920
|
'--pass <stamp>',
|
|
842
921
|
'The instant the pass started, in compact UTC such as 20261002T154450Z, with --add',
|
|
843
922
|
)
|
|
923
|
+
.option(
|
|
924
|
+
'--theme <theme>',
|
|
925
|
+
'The theme the frame was shot at, dark or light, with --add, keeping two shots of one box apart',
|
|
926
|
+
)
|
|
844
927
|
.option('--drop', "Rewrite the branch without this pull request's frames")
|
|
845
928
|
.option(
|
|
846
929
|
'--prune <days>',
|
|
@@ -852,7 +935,8 @@ export function register(program: Command): void {
|
|
|
852
935
|
'',
|
|
853
936
|
`Writes only refs/heads/${FRAMES_BRANCH}, through the GitHub git data API,`,
|
|
854
937
|
'so nothing is checked out and no other branch is touched. A frame lands',
|
|
855
|
-
'at pr-<number>/<short-head>/<pass>/box-<n>.png,
|
|
938
|
+
'at pr-<number>/<short-head>/<pass>/box-<n>.png, or box-<n>-<theme>.png',
|
|
939
|
+
'with --theme, and `link` names that',
|
|
856
940
|
`path on ${FRAMES_BRANCH}, so it survives a rewrite that drops other pull`,
|
|
857
941
|
'requests and resolves until its own pull request is dropped. The pass',
|
|
858
942
|
'segment keeps a second pass at one head and box from replacing an image',
|
|
@@ -871,6 +955,7 @@ export function register(program: Command): void {
|
|
|
871
955
|
' bad-box --add without a positive --box',
|
|
872
956
|
' bad-head --head is not 7 to 40 lowercase hex characters',
|
|
873
957
|
' bad-pass --add without a compact UTC --pass stamp',
|
|
958
|
+
' bad-theme --theme is not dark or light, or no --add',
|
|
874
959
|
' bad-days --prune without a positive number of days',
|
|
875
960
|
' unreadable-frame the --add file is missing, empty, or not a PNG',
|
|
876
961
|
' read-only GitHub refused the write, as on a fork token',
|
|
@@ -2906,6 +2991,12 @@ function framesArgumentRefusal(
|
|
|
2906
2991
|
) {
|
|
2907
2992
|
return 'bad-pass'
|
|
2908
2993
|
}
|
|
2994
|
+
if (
|
|
2995
|
+
opts.theme !== undefined &&
|
|
2996
|
+
(opts.add === undefined || !isFrameTheme(opts.theme))
|
|
2997
|
+
) {
|
|
2998
|
+
return 'bad-theme'
|
|
2999
|
+
}
|
|
2909
3000
|
return undefined
|
|
2910
3001
|
}
|
|
2911
3002
|
|
|
@@ -3005,11 +3096,13 @@ async function runFrames(
|
|
|
3005
3096
|
|
|
3006
3097
|
const box = Number(opts.box)
|
|
3007
3098
|
const pass = opts.pass ?? ''
|
|
3099
|
+
const theme = opts.theme as FrameTheme | undefined
|
|
3008
3100
|
const result = await pushFrame(api, {
|
|
3009
3101
|
number: pull,
|
|
3010
3102
|
head,
|
|
3011
3103
|
pass,
|
|
3012
3104
|
box,
|
|
3105
|
+
theme,
|
|
3013
3106
|
bytes: bytes ?? new Uint8Array(),
|
|
3014
3107
|
})
|
|
3015
3108
|
if (result.kind === 'refused') return refuseWrite(result.reason)
|
|
@@ -3018,6 +3111,7 @@ async function runFrames(
|
|
|
3018
3111
|
{
|
|
3019
3112
|
number: pull,
|
|
3020
3113
|
box,
|
|
3114
|
+
...(theme === undefined ? {} : { theme }),
|
|
3021
3115
|
head,
|
|
3022
3116
|
pass,
|
|
3023
3117
|
path: result.path,
|
|
@@ -3029,6 +3123,287 @@ async function runFrames(
|
|
|
3029
3123
|
)
|
|
3030
3124
|
}
|
|
3031
3125
|
|
|
3126
|
+
const LIFT_REFUSALS: Record<LiftRefusal | 'bad-wait' | 'gh-missing', string> = {
|
|
3127
|
+
'bad-wait': '--wait takes a whole number of seconds, zero or more.',
|
|
3128
|
+
'gh-missing': PULL_REFUSALS['gh-missing'],
|
|
3129
|
+
'pull-closed': 'The pull request is closed, so there is no mark to lift.',
|
|
3130
|
+
'review-missing': 'The thread carries no code review, so none has closed.',
|
|
3131
|
+
'review-open':
|
|
3132
|
+
'The newest code review is open, so a finding still owes a fix.',
|
|
3133
|
+
'review-unmarked':
|
|
3134
|
+
'The newest code review carries no read-time marker, so the commit it covers is a GitHub stamp this gate does not trust.',
|
|
3135
|
+
'review-behind':
|
|
3136
|
+
'The newest code review closed on an earlier commit than the head.',
|
|
3137
|
+
'ui-missing':
|
|
3138
|
+
'The evidence comment carries a checklist and no UI review has been posted.',
|
|
3139
|
+
'ui-open': 'The newest UI review is open, so a box still owes a fix.',
|
|
3140
|
+
'ui-unmarked':
|
|
3141
|
+
'The newest UI review carries no head marker, so the commit it covers is a GitHub stamp this gate does not trust.',
|
|
3142
|
+
'ui-behind':
|
|
3143
|
+
'The newest UI review closed on an earlier commit than the head.',
|
|
3144
|
+
'evidence-owed':
|
|
3145
|
+
'The evidence check reads owed, so the ship chain left a step undone.',
|
|
3146
|
+
'standing-stale':
|
|
3147
|
+
'The base moved under a path this branch writes since it branched, so CI measured a tree that is gone.',
|
|
3148
|
+
'standing-conflict': 'The head does not merge cleanly into the base.',
|
|
3149
|
+
'standing-unknown':
|
|
3150
|
+
'The head or the base did not resolve, which a fork or a deleted head leaves, so its standing is unread and waits on a person.',
|
|
3151
|
+
'ci-pending': 'A check run on the tip has yet to conclude.',
|
|
3152
|
+
'ci-failing': 'A check run on the tip failed.',
|
|
3153
|
+
'ci-conflicted':
|
|
3154
|
+
'The branch conflicts with its base, so no run will start for the tip.',
|
|
3155
|
+
'ci-timeout': 'A check run was still pending when --wait ran out.',
|
|
3156
|
+
'head-moved':
|
|
3157
|
+
'A push landed during the wait, so the gates cleared a commit that is no longer the head.',
|
|
3158
|
+
'gh-failed':
|
|
3159
|
+
'A read or the mutation was refused, so nothing is reported as lifted.',
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const READY_MUTATION =
|
|
3163
|
+
'mutation($id: ID!) { markPullRequestReadyForReview(input: {pullRequestId: $id}) { pullRequest { isDraft } } }'
|
|
3164
|
+
|
|
3165
|
+
function gitRunner(root: string): GitRunner {
|
|
3166
|
+
return async (args) => {
|
|
3167
|
+
const result = await execa('git', ['-C', root, ...args], {
|
|
3168
|
+
reject: false,
|
|
3169
|
+
timeout: GH_TIMEOUT_MS,
|
|
3170
|
+
env: gitEnv(),
|
|
3171
|
+
extendEnv: false,
|
|
3172
|
+
})
|
|
3173
|
+
return result.exitCode === 0 ? String(result.stdout) : undefined
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Reads the evidence check through this checkout's own CLI rather than a
|
|
3179
|
+
* second copy of its logic, so the lift and a caller running
|
|
3180
|
+
* `canon pr evidence --check` read one answer.
|
|
3181
|
+
*/
|
|
3182
|
+
async function readEvidenceGate(
|
|
3183
|
+
root: string,
|
|
3184
|
+
number: string,
|
|
3185
|
+
): Promise<EvidenceGate | undefined> {
|
|
3186
|
+
const result = await execa(
|
|
3187
|
+
process.execPath,
|
|
3188
|
+
[
|
|
3189
|
+
join(PROJECT_ROOT, 'src', 'cli.ts'),
|
|
3190
|
+
'pr',
|
|
3191
|
+
'evidence',
|
|
3192
|
+
number,
|
|
3193
|
+
'--check',
|
|
3194
|
+
'--json',
|
|
3195
|
+
'--root',
|
|
3196
|
+
root,
|
|
3197
|
+
],
|
|
3198
|
+
{
|
|
3199
|
+
cwd: root,
|
|
3200
|
+
reject: false,
|
|
3201
|
+
timeout: GH_TIMEOUT_MS * 2,
|
|
3202
|
+
env: { ...gitEnv(), CANON_NON_INTERACTIVE: '1' },
|
|
3203
|
+
extendEnv: false,
|
|
3204
|
+
},
|
|
3205
|
+
)
|
|
3206
|
+
const last = String(result.stdout)
|
|
3207
|
+
.split('\n')
|
|
3208
|
+
.filter((line) => line.trim() !== '')
|
|
3209
|
+
.at(-1)
|
|
3210
|
+
if (last === undefined) return undefined
|
|
3211
|
+
|
|
3212
|
+
let record: { reason?: unknown; owed?: unknown; checklist?: unknown }
|
|
3213
|
+
try {
|
|
3214
|
+
record = JSON.parse(last)
|
|
3215
|
+
} catch {
|
|
3216
|
+
return undefined
|
|
3217
|
+
}
|
|
3218
|
+
if (record.reason !== 'settled' && record.reason !== 'owed') return undefined
|
|
3219
|
+
|
|
3220
|
+
return {
|
|
3221
|
+
isSettled: record.reason === 'settled',
|
|
3222
|
+
owed: Array.isArray(record.owed) ? record.owed.map(String) : [],
|
|
3223
|
+
hasChecklist:
|
|
3224
|
+
typeof record.checklist === 'string' && record.checklist !== '',
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function liftApi(root: string, number: string, baseRef: string): LiftApi {
|
|
3229
|
+
const git = gitRunner(root)
|
|
3230
|
+
|
|
3231
|
+
return {
|
|
3232
|
+
readPull: async (): Promise<PullState | undefined> => {
|
|
3233
|
+
const stdout = await gh(root, [
|
|
3234
|
+
'api',
|
|
3235
|
+
`repos/{owner}/{repo}/pulls/${number}`,
|
|
3236
|
+
])
|
|
3237
|
+
if (stdout === null) return undefined
|
|
3238
|
+
try {
|
|
3239
|
+
const row = JSON.parse(stdout) as RestPull
|
|
3240
|
+
if (row.node_id === undefined || row.head?.ref === undefined) {
|
|
3241
|
+
return undefined
|
|
3242
|
+
}
|
|
3243
|
+
return {
|
|
3244
|
+
isDraft: row.draft === true,
|
|
3245
|
+
isOpen: row.state === 'open',
|
|
3246
|
+
nodeId: row.node_id,
|
|
3247
|
+
branch: row.head.ref,
|
|
3248
|
+
head: row.head.sha ?? '',
|
|
3249
|
+
}
|
|
3250
|
+
} catch {
|
|
3251
|
+
return undefined
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
readTip: async (branch) => {
|
|
3255
|
+
const resolved = await resolveTip(branch, refReader(root))
|
|
3256
|
+
return resolved.kind === 'read' ? resolved.tip : undefined
|
|
3257
|
+
},
|
|
3258
|
+
readReviews: async () => {
|
|
3259
|
+
const reviews = await listReviews(root, number)
|
|
3260
|
+
return reviews.kind === 'read' ? reviews.rows.map(reviewRowOf) : undefined
|
|
3261
|
+
},
|
|
3262
|
+
readEvidence: () => readEvidenceGate(root, number),
|
|
3263
|
+
readStanding: (head) =>
|
|
3264
|
+
readBaseStanding(git, Number(number), head, baseRef),
|
|
3265
|
+
readChecks: async (tip) => {
|
|
3266
|
+
const [listed, pulled] = await Promise.all([
|
|
3267
|
+
gh(root, [
|
|
3268
|
+
'api',
|
|
3269
|
+
`repos/{owner}/{repo}/commits/${tip}/check-runs?per_page=100`,
|
|
3270
|
+
]),
|
|
3271
|
+
gh(root, ['api', `repos/{owner}/{repo}/pulls/${number}`]),
|
|
3272
|
+
])
|
|
3273
|
+
if (listed === null) return undefined
|
|
3274
|
+
try {
|
|
3275
|
+
const mergeState =
|
|
3276
|
+
pulled === null
|
|
3277
|
+
? undefined
|
|
3278
|
+
: mergeStateOf((JSON.parse(pulled) as RestPull).mergeable_state)
|
|
3279
|
+
const reading = withMergeState(
|
|
3280
|
+
mergeState,
|
|
3281
|
+
collapseChecks(tip, JSON.parse(listed) as CheckRunListing),
|
|
3282
|
+
)
|
|
3283
|
+
return { state: reading.state, isConflicted: reading.conflicted }
|
|
3284
|
+
} catch {
|
|
3285
|
+
return undefined
|
|
3286
|
+
}
|
|
3287
|
+
},
|
|
3288
|
+
markReady: async (nodeId) => {
|
|
3289
|
+
const result = await execa(
|
|
3290
|
+
'gh',
|
|
3291
|
+
[
|
|
3292
|
+
'api',
|
|
3293
|
+
'graphql',
|
|
3294
|
+
'-f',
|
|
3295
|
+
`query=${READY_MUTATION}`,
|
|
3296
|
+
'-f',
|
|
3297
|
+
`id=${nodeId}`,
|
|
3298
|
+
],
|
|
3299
|
+
{
|
|
3300
|
+
cwd: root,
|
|
3301
|
+
reject: false,
|
|
3302
|
+
timeout: GH_TIMEOUT_MS,
|
|
3303
|
+
env: gitEnv(),
|
|
3304
|
+
extendEnv: false,
|
|
3305
|
+
},
|
|
3306
|
+
)
|
|
3307
|
+
if (result.exitCode !== 0) return false
|
|
3308
|
+
// The flag is read back rather than the exit trusted, since the
|
|
3309
|
+
// mutation can answer 200 with an errors array and no change.
|
|
3310
|
+
try {
|
|
3311
|
+
const body = JSON.parse(String(result.stdout)) as {
|
|
3312
|
+
data?: {
|
|
3313
|
+
markPullRequestReadyForReview?: {
|
|
3314
|
+
pullRequest?: { isDraft?: boolean }
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return (
|
|
3319
|
+
body.data?.markPullRequestReadyForReview?.pullRequest?.isDraft ===
|
|
3320
|
+
false
|
|
3321
|
+
)
|
|
3322
|
+
} catch {
|
|
3323
|
+
return false
|
|
3324
|
+
}
|
|
3325
|
+
},
|
|
3326
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
3327
|
+
now: () => Date.now(),
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
async function runLift(
|
|
3332
|
+
number: string | undefined,
|
|
3333
|
+
opts: LiftCommandOptions,
|
|
3334
|
+
): Promise<number> {
|
|
3335
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
3336
|
+
const emitJson = opts.json ?? false
|
|
3337
|
+
|
|
3338
|
+
intro('canon pr lift')
|
|
3339
|
+
|
|
3340
|
+
let waitSeconds = DEFAULT_WAIT_SECONDS
|
|
3341
|
+
if (opts.wait !== undefined) {
|
|
3342
|
+
if (!/^\d+$/.test(opts.wait)) {
|
|
3343
|
+
return refuseWith('bad-wait', LIFT_REFUSALS['bad-wait'], emitJson, root)
|
|
3344
|
+
}
|
|
3345
|
+
waitSeconds = Number(opts.wait)
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
if (Bun.which('gh') === null) {
|
|
3349
|
+
return refuseWith('gh-missing', LIFT_REFUSALS['gh-missing'], emitJson, root)
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
const resolved = await resolvePullNumber(root, number)
|
|
3353
|
+
if (resolved.kind === 'refused') {
|
|
3354
|
+
return refuseWith(
|
|
3355
|
+
resolved.reason,
|
|
3356
|
+
PULL_REFUSALS[resolved.reason],
|
|
3357
|
+
emitJson,
|
|
3358
|
+
root,
|
|
3359
|
+
)
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// Read once up front for the base the standing is measured against. The
|
|
3363
|
+
// injected `readPull` re-reads the object on every gate that needs it.
|
|
3364
|
+
const initial = await readPull(root, resolved.number)
|
|
3365
|
+
if (initial.kind === 'refused') {
|
|
3366
|
+
return refuseWith(
|
|
3367
|
+
initial.reason,
|
|
3368
|
+
PULL_REFUSALS[initial.reason],
|
|
3369
|
+
emitJson,
|
|
3370
|
+
root,
|
|
3371
|
+
)
|
|
3372
|
+
}
|
|
3373
|
+
const base = initial.row.base?.ref
|
|
3374
|
+
if (base === undefined || base === '') {
|
|
3375
|
+
return refuseWith('gh-failed', LIFT_REFUSALS['gh-failed'], emitJson, root)
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
const record = await lift(liftApi(root, resolved.number, `origin/${base}`), {
|
|
3379
|
+
isCheck: opts.check ?? false,
|
|
3380
|
+
waitSeconds,
|
|
3381
|
+
})
|
|
3382
|
+
|
|
3383
|
+
logStep(record.reason)
|
|
3384
|
+
logInfo(
|
|
3385
|
+
record.reason === 'lifted'
|
|
3386
|
+
? `#${resolved.number} is ready for review at ${record.head?.slice(0, 8)}`
|
|
3387
|
+
: record.reason === 'already-ready'
|
|
3388
|
+
? `#${resolved.number} is already ready for review`
|
|
3389
|
+
: record.reason === 'clear'
|
|
3390
|
+
? `every gate reads clear for #${resolved.number}`
|
|
3391
|
+
: LIFT_REFUSALS[record.reason],
|
|
3392
|
+
)
|
|
3393
|
+
for (const gate of record.gates) {
|
|
3394
|
+
if (!gate.isOk) logWarn(`${gate.gate}: ${gate.reason ?? 'not clear'}`)
|
|
3395
|
+
}
|
|
3396
|
+
outro()
|
|
3397
|
+
|
|
3398
|
+
if (emitJson) {
|
|
3399
|
+
process.stdout.write(
|
|
3400
|
+
`${JSON.stringify({ root, number: Number(resolved.number), ...record })}\n`,
|
|
3401
|
+
)
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
return record.isOk ? 0 : 1
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3032
3407
|
/**
|
|
3033
3408
|
* Frames a refusal on stderr in both modes and puts the record on stdout alone,
|
|
3034
3409
|
* so an operator reading the terminal sees the reason rather than a command
|