@erclx/canon 5.68.0 → 5.70.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/CHANGELOG.md +36 -0
- package/README.md +33 -159
- package/assets/brand/mark.svg +17 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +5 -5
- package/claude/skills/app-drive/SKILL.md +17 -6
- package/claude/skills/app-drive/references/drive-record.md +61 -8
- package/claude/skills/app-drive/references/isolate.md +1 -1
- package/claude/skills/canon-cli/SKILL.md +2 -2
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback/SKILL.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +4 -4
- package/claude/skills/design-identity/SKILL.md +7 -7
- package/claude/skills/{pick-by-eye → design-pick}/REQUIREMENT.md +6 -6
- package/claude/skills/{pick-by-eye → design-pick}/SKILL.md +19 -10
- package/claude/skills/{pick-by-eye → design-pick}/references/board.md +17 -11
- package/claude/skills/design-pick/references/design-config.md +33 -0
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -2
- package/claude/skills/design-taste/SKILL.md +3 -3
- package/claude/skills/design-taste/references/preflight.md +1 -1
- package/claude/skills/git-issue/REQUIREMENT.md +1 -1
- package/claude/skills/git-issue/references/issue.md +1 -1
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/plan-feature/references/plan.md +21 -6
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/pr-evidence/references/evidence.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +3 -3
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-planning-wait.md +24 -0
- package/claude/skills/role-planner/SKILL.md +4 -2
- package/claude/skills/role-worker/SKILL.md +5 -6
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/references/receipt-sweep.md +1 -1
- package/claude/skills/{ux-audit → ui-audit}/REQUIREMENT.md +3 -3
- package/claude/skills/{ux-audit → ui-audit}/SKILL.md +7 -7
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-craft/SKILL.md +1 -1
- package/claude/skills/ui-craft/references/copy.md +4 -1
- package/claude/skills/{ui-record → ui-film}/REQUIREMENT.md +6 -4
- package/claude/skills/{ui-record → ui-film}/SKILL.md +26 -13
- package/claude/skills/ui-film/references/compose-craft.md +81 -0
- package/claude/skills/{ui-record → ui-film}/references/config.md +9 -4
- package/claude/skills/{ui-record → ui-film}/references/handoff.md +17 -7
- package/claude/skills/{ui-record → ui-film}/references/pr-clip.md +5 -5
- package/claude/skills/{ui-record → ui-film}/references/take-craft.md +28 -2
- package/claude/skills/{ux-measure → ui-measure}/REQUIREMENT.md +3 -3
- package/claude/skills/{ux-measure → ui-measure}/SKILL.md +6 -6
- package/docs/agents/audits.md +1 -1
- package/docs/agents/migrate.md +13 -13
- package/docs/index.md +1 -0
- package/docs/migrations/by-hand.md +107 -0
- package/docs/migrations/by-verb.md +118 -0
- package/docs/migrations/index.md +14 -0
- package/docs/target/index.md +1 -2
- package/docs/target/projects.md +3 -3
- package/docs/target/sync.md +23 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/skill-map-on-request.md +1 -2
- package/docs/workflow/skill-map.md +9 -10
- package/docs/workflow/visual-design-workflow.md +6 -1
- package/governance/ledger/claude/background.md +26 -0
- package/governance/ledger/skills/ui-craft/copy.md +8 -4
- package/governance/rules/claude/background.md +19 -0
- package/governance/stacks/base.toml +1 -0
- package/package.json +6 -4
- package/scripts/ci/install-playwright-deps.sh +52 -0
- package/scripts/core/design-sync.ts +204 -0
- package/scripts/core/regen-web-favicon.ts +27 -12
- package/scripts/core/regen-web-previews.ts +8 -1
- package/scripts/core/render-film.ts +179 -0
- package/src/capture/frame-values.ts +12 -12
- package/src/claude/cases/authoring.ts +2 -2
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +0 -5
- package/src/claude/cases/workflow.ts +6 -6
- package/src/commands/fixtures/gh.sh +64 -0
- package/src/commands/gov.ts +33 -3
- package/src/commands/migrate.ts +12 -2
- package/src/commands/records.ts +3 -2
- package/src/commands/sandbox.ts +9 -2
- package/src/commands/take.ts +159 -11
- package/src/commands/tasks.ts +183 -0
- package/src/design/favicon.ts +85 -13
- package/src/design/reference.ts +98 -0
- package/src/design/render.ts +16 -14
- package/src/design/tokens.ts +4 -3
- package/src/gate/stages.ts +8 -2
- package/src/gov/audit.ts +48 -11
- package/src/gov/detect.ts +1 -1
- package/src/init/steps.ts +2 -2
- package/src/markdown/scope.ts +9 -14
- package/src/migrate/record-layout.ts +5 -3
- package/src/migrate/skill-names.ts +21 -4
- package/src/migrate/status.ts +3 -3
- package/src/records/validate.ts +15 -2
- package/src/sandbox/expect.ts +32 -0
- package/src/sandbox/headless/dispatch-shim.sh +15 -0
- package/src/sandbox/headless/dispatch.ts +12 -14
- package/src/sandbox/headless/run.ts +6 -3
- package/src/sandbox/stub-gh.sh +26 -0
- package/src/sandbox/stub-remote.ts +8 -29
- package/src/sync/engine.ts +1 -1
- package/src/take/camera.ts +100 -11
- package/src/take/film.ts +55 -4
- package/src/take/fixtures/input.html +138 -0
- package/src/take/keys.ts +193 -0
- package/src/take/log.ts +94 -1
- package/src/take/perform.ts +165 -10
- package/src/take/plan.ts +101 -6
- package/src/take/pointer.ts +2 -1
- package/src/take/screencast.ts +28 -3
- package/src/take/stepper.ts +141 -23
- package/src/take/zoom/NOTICE.md +1 -1
- package/src/tasks/launch-wait.ts +160 -0
- package/tooling/claude/reference.md +1 -1
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +0 -34
- package/claude/skills/canon-feedback-triage/SKILL.md +0 -98
- package/claude/skills/plan-ready/REQUIREMENT.md +0 -35
- package/claude/skills/plan-ready/SKILL.md +0 -136
- package/claude/skills/plan-ready/references/assembly.md +0 -75
- package/claude/skills/plan-ready/references/ready.md +0 -128
- package/docs/target/migrations.md +0 -203
- /package/claude/skills/{pick-by-eye → design-pick}/references/live-arms.md +0 -0
package/src/commands/gov.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from '@/gov/restated'
|
|
35
35
|
import {
|
|
36
36
|
auditExit,
|
|
37
|
+
CORE_FOLDERS,
|
|
37
38
|
hasFinding,
|
|
38
39
|
PRACTICE_FOLDERS,
|
|
39
40
|
readRuleAudit,
|
|
@@ -62,6 +63,11 @@ import {
|
|
|
62
63
|
readTestOrder,
|
|
63
64
|
type TestOrderReport,
|
|
64
65
|
} from '@/gov/test-order'
|
|
66
|
+
import {
|
|
67
|
+
type ChangedSet,
|
|
68
|
+
collectChangedFiles,
|
|
69
|
+
commandRunner,
|
|
70
|
+
} from '@/gate/sequencer'
|
|
65
71
|
import { recordStamp, runDomainSync } from '@/sync/engine'
|
|
66
72
|
import { detectRules } from '@/gov/detect'
|
|
67
73
|
import { writeChainStamp } from '@/sync/stamp'
|
|
@@ -138,6 +144,7 @@ interface AuditOptions {
|
|
|
138
144
|
readonly root?: string
|
|
139
145
|
readonly json?: boolean
|
|
140
146
|
readonly gate?: boolean
|
|
147
|
+
readonly touched?: boolean
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/** What a reader does about each way the sweep produced no reading. */
|
|
@@ -613,6 +620,10 @@ export function register(program: Command): void {
|
|
|
613
620
|
'--gate',
|
|
614
621
|
'Set 2 only for a finding in a gated folder, which the push stage reads',
|
|
615
622
|
)
|
|
623
|
+
.option(
|
|
624
|
+
'--touched',
|
|
625
|
+
'Owe a ledger for each core rule the branch changed against origin/main',
|
|
626
|
+
)
|
|
616
627
|
.addHelpText(
|
|
617
628
|
'after',
|
|
618
629
|
[
|
|
@@ -632,6 +643,9 @@ export function register(program: Command): void {
|
|
|
632
643
|
` asks a ledger of each rule under ${PRACTICE_FOLDERS.join(', ')}`,
|
|
633
644
|
' at governance/ledger/<folder>/<slug>.md, one table row per bullet',
|
|
634
645
|
' under a heading of the same name.',
|
|
646
|
+
` A rule under ${CORE_FOLDERS.join(', ')} has its ledger checked where`,
|
|
647
|
+
' one exists, and under --touched owes one once the branch changed it.',
|
|
648
|
+
' With no baseline against main, --touched reads no rule as touched.',
|
|
635
649
|
' Any other tree reads .claude/rules/project/ alone and asks no ledger.',
|
|
636
650
|
' .claude/rules/canon/ is fixed at the toolkit source and never read',
|
|
637
651
|
' unless named. A named path resolves against --root and must sit',
|
|
@@ -654,6 +668,7 @@ export function register(program: Command): void {
|
|
|
654
668
|
' canon gov audit',
|
|
655
669
|
' canon gov audit .claude/rules/project/api.md',
|
|
656
670
|
' canon gov audit --json',
|
|
671
|
+
' canon gov audit --gate --touched',
|
|
657
672
|
'',
|
|
658
673
|
].join('\n'),
|
|
659
674
|
)
|
|
@@ -672,7 +687,13 @@ async function runAudit(
|
|
|
672
687
|
opts: AuditOptions,
|
|
673
688
|
): Promise<number> {
|
|
674
689
|
const root = resolve(opts.root ?? process.cwd())
|
|
675
|
-
const
|
|
690
|
+
const changed = opts.touched
|
|
691
|
+
? await collectChangedFiles(commandRunner(root))
|
|
692
|
+
: undefined
|
|
693
|
+
// An unscoped set lists nothing, and reading it as every file would owe a
|
|
694
|
+
// ledger of every core rule on a branch that touched none of them.
|
|
695
|
+
const touched = changed?.scoped ? changed.files : []
|
|
696
|
+
const report = await readRuleAudit(root, paths, { touched })
|
|
676
697
|
const emitJson = opts.json ?? false
|
|
677
698
|
|
|
678
699
|
if (report.kind === 'unreadable') {
|
|
@@ -690,7 +711,7 @@ async function runAudit(
|
|
|
690
711
|
return 1
|
|
691
712
|
}
|
|
692
713
|
|
|
693
|
-
reportAudit(report, root, paths.length > 0)
|
|
714
|
+
reportAudit(report, root, paths.length > 0, changed)
|
|
694
715
|
|
|
695
716
|
if (emitJson) {
|
|
696
717
|
process.stdout.write(
|
|
@@ -700,6 +721,9 @@ async function runAudit(
|
|
|
700
721
|
limits: RULE_LIMITS,
|
|
701
722
|
gated: RULE_GATED_FOLDERS,
|
|
702
723
|
exempt: RULE_GATE_EXEMPT,
|
|
724
|
+
...(changed === undefined
|
|
725
|
+
? {}
|
|
726
|
+
: { touched: { scoped: changed.scoped } }),
|
|
703
727
|
...report,
|
|
704
728
|
})}\n`,
|
|
705
729
|
)
|
|
@@ -728,6 +752,7 @@ function reportAudit(
|
|
|
728
752
|
report: Extract<RuleAuditReport, { kind: 'measured' }>,
|
|
729
753
|
root: string,
|
|
730
754
|
named: boolean,
|
|
755
|
+
changed: ChangedSet | undefined,
|
|
731
756
|
): void {
|
|
732
757
|
intro('canon gov audit')
|
|
733
758
|
|
|
@@ -761,10 +786,15 @@ function reportAudit(
|
|
|
761
786
|
}
|
|
762
787
|
|
|
763
788
|
logStep('Ledgers')
|
|
789
|
+
if (changed !== undefined && !changed.scoped) {
|
|
790
|
+
logInfo(
|
|
791
|
+
'no baseline against main, so --touched reads no core rule as touched',
|
|
792
|
+
)
|
|
793
|
+
}
|
|
764
794
|
if (report.scope === 'target') {
|
|
765
795
|
logInfo('not read, since ledgers live in the toolkit and never ship')
|
|
766
796
|
} else if (ledgers.length === 0) {
|
|
767
|
-
logInfo('every
|
|
797
|
+
logInfo('every rule owing a ledger has one, with a row per bullet')
|
|
768
798
|
} else {
|
|
769
799
|
for (const line of ledgers) logWarn(line)
|
|
770
800
|
}
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1309,6 +1309,13 @@ export function register(program: Command): void {
|
|
|
1309
1309
|
'any ordinal evidence/ already carries. The dry run labels every',
|
|
1310
1310
|
'derived destination, so check the split before passing --write.',
|
|
1311
1311
|
'',
|
|
1312
|
+
'Moves the design board to the folder Frameboard names:',
|
|
1313
|
+
' canvas/ -> board/',
|
|
1314
|
+
'',
|
|
1315
|
+
'Stop a frameboard serve on canvas/ before --write. A project holding',
|
|
1316
|
+
'both folders gets a collision, and a design.toml board key naming',
|
|
1317
|
+
'.canon/canvas is left for you to update.',
|
|
1318
|
+
'',
|
|
1312
1319
|
'A citation reaching into a moved folder or naming a moved file is',
|
|
1313
1320
|
'repointed, live or archived. A bare mention of an emptied folder, such',
|
|
1314
1321
|
'as review/evidence/ with no slug, matches no row and stays as written.',
|
|
@@ -1389,8 +1396,11 @@ export function register(program: Command): void {
|
|
|
1389
1396
|
`It also carries ${Object.keys(SKILL_CITATION_MAP).length} namespaced-only rows for a skill renamed from a single word.`,
|
|
1390
1397
|
'The seven draft- skills move onto verb-first names, such as',
|
|
1391
1398
|
// The old spellings below are the rename itself. canon-keep-retired
|
|
1392
|
-
'draft-doc to write-doc and draft-
|
|
1393
|
-
'pointer left under each old name is never moved.',
|
|
1399
|
+
'draft-doc to write-doc and draft-slides to make-deck, and a',
|
|
1400
|
+
'pointer left under each old name is never moved. Four skills then',
|
|
1401
|
+
'moved into the family their job belongs to, such as',
|
|
1402
|
+
// canon-keep-retired
|
|
1403
|
+
'ux-audit to ui-audit and pick-by-eye to design-pick.',
|
|
1394
1404
|
'It takes no scope. The skill folders it moves are authored in the',
|
|
1395
1405
|
'toolkit and no target holds a copy of that catalog, so in a target it',
|
|
1396
1406
|
'rewrites citations of a renamed skill and moves nothing.',
|
package/src/commands/records.ts
CHANGED
|
@@ -110,8 +110,9 @@ export function register(program: Command): void {
|
|
|
110
110
|
'',
|
|
111
111
|
'Structure, under `structure` and never in the exit:',
|
|
112
112
|
" S1 to S5 from the plan standard, read on plans, a track's README and",
|
|
113
|
-
|
|
114
|
-
'
|
|
113
|
+
" numbered files, intake clusters, and an intake folder's",
|
|
114
|
+
' evidence/drive-log.md, plus intake-spill for content under a field.',
|
|
115
|
+
' A record written before the criteria misses S1, so it reports alone.',
|
|
115
116
|
'',
|
|
116
117
|
'Refusals, read off `reason`:',
|
|
117
118
|
" no-folder none of the kind's directories exist at either record root",
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type RunEnvelope,
|
|
26
26
|
type Verdict,
|
|
27
27
|
} from '@/sandbox/expect'
|
|
28
|
-
import { runHeadless } from '@/sandbox/headless/run'
|
|
28
|
+
import { runHeadless, runSettings } from '@/sandbox/headless/run'
|
|
29
29
|
import { runSandbox } from '@/sandbox/provision'
|
|
30
30
|
import {
|
|
31
31
|
EXPECTED_FILE,
|
|
@@ -120,7 +120,12 @@ function showProvisionHelp(): void {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** A run that reported nothing counts as clean, so only real signals fail. */
|
|
123
|
-
const CLEAN_ENVELOPE: RunEnvelope = {
|
|
123
|
+
const CLEAN_ENVELOPE: RunEnvelope = {
|
|
124
|
+
isError: false,
|
|
125
|
+
isTruncated: false,
|
|
126
|
+
turns: 0,
|
|
127
|
+
denials: 0,
|
|
128
|
+
}
|
|
124
129
|
|
|
125
130
|
interface CheckOptions {
|
|
126
131
|
readonly envelope?: string
|
|
@@ -190,6 +195,8 @@ function readEnvelope(path: string | undefined): RunEnvelope | undefined {
|
|
|
190
195
|
|
|
191
196
|
return {
|
|
192
197
|
isError: parsed.is_error === true,
|
|
198
|
+
isTruncated: parsed.subtype === 'error_max_turns',
|
|
199
|
+
turnCap: Number(runSettings(process.env).maxTurns),
|
|
193
200
|
turns: typeof parsed.num_turns === 'number' ? parsed.num_turns : 0,
|
|
194
201
|
denials: Array.isArray(denials) ? denials.length : 0,
|
|
195
202
|
reply: typeof parsed.result === 'string' ? parsed.result : undefined,
|
package/src/commands/take.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { INSTALL_BROWSER, isEngineMissing } from '@/browser/engine'
|
|
5
|
+
import { cameraTrack, readRate } from '@/take/camera'
|
|
6
|
+
import { readTakeLog } from '@/take/log'
|
|
5
7
|
import { readTakePlan } from '@/take/plan'
|
|
6
8
|
import type { TakePlanRefusal } from '@/take/plan'
|
|
7
9
|
import type { TakeRefusal } from '@/take/film'
|
|
@@ -24,11 +26,17 @@ import {
|
|
|
24
26
|
type Take = typeof import('@/take/film')
|
|
25
27
|
|
|
26
28
|
interface RunOptions {
|
|
27
|
-
readonly out
|
|
29
|
+
readonly out?: string
|
|
28
30
|
readonly reel?: string
|
|
29
31
|
readonly json?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
interface CameraOptions {
|
|
35
|
+
readonly out?: string
|
|
36
|
+
readonly rate?: string
|
|
37
|
+
readonly json?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
const INSTALL_FFMPEG =
|
|
33
41
|
'install FFmpeg from your package manager, such as apt install ffmpeg or brew install ffmpeg'
|
|
34
42
|
|
|
@@ -40,7 +48,8 @@ const PLAN_REFUSALS: Record<TakePlanRefusal, string> = {
|
|
|
40
48
|
'bad-beat':
|
|
41
49
|
'A beat is missing its name or carries a setup that is not a list.',
|
|
42
50
|
'no-steps': 'A beat names no step, so its take would film nothing.',
|
|
43
|
-
'unknown-route':
|
|
51
|
+
'unknown-route':
|
|
52
|
+
'The plan or a beat names a route this build does not record on.',
|
|
44
53
|
'bad-step': 'A step is missing something the take needs to perform it.',
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -53,14 +62,19 @@ const TAKE_REFUSALS: Record<TakeRefusal, string> = {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export function register(program: Command): void {
|
|
56
|
-
program
|
|
65
|
+
const take = program
|
|
57
66
|
.command('take')
|
|
58
67
|
.description(
|
|
59
68
|
'Film a web app beat by beat with a click log and camera track',
|
|
60
69
|
)
|
|
61
70
|
.argument('<url>', 'Address to film')
|
|
62
71
|
.argument('<plan>', 'JSON file naming the beats and their steps')
|
|
63
|
-
|
|
72
|
+
// Checked in the action rather than required here, since commander checks
|
|
73
|
+
// a parent's required options on every subcommand, `camera` included.
|
|
74
|
+
.option('--out <dir>', 'Folder the clips, logs, and tracks land in')
|
|
75
|
+
// Keeps this command's options ahead of `camera`, which reuses --out and
|
|
76
|
+
// --json for its own.
|
|
77
|
+
.enablePositionalOptions()
|
|
64
78
|
.helpOption('-h, --help', 'Show this help message')
|
|
65
79
|
.option(
|
|
66
80
|
'--reel <file>',
|
|
@@ -106,12 +120,33 @@ export function register(program: Command): void {
|
|
|
106
120
|
'Step kinds:',
|
|
107
121
|
' move <target>|<x,y> glide the pointer to a target or a point',
|
|
108
122
|
' click <target> glide to the target and press it',
|
|
123
|
+
' dblclick <target> glide to the target and press it twice',
|
|
124
|
+
' drag <from> <to> press the from target, glide held, release on to',
|
|
125
|
+
' key <keys> press a combination such as Control+K',
|
|
109
126
|
' fill <target> <text> press the target and type the text',
|
|
110
127
|
' scroll <target> <dy> glide to the target and wheel dy pixels',
|
|
111
128
|
' wait <ms> hold, for a state the page reaches on its own',
|
|
112
129
|
'',
|
|
113
|
-
'
|
|
114
|
-
'
|
|
130
|
+
'click, dblclick, and drag take modifiers, a list of Alt, Control, Meta,',
|
|
131
|
+
'and Shift held through the press, as in "modifiers": ["Shift"]. keys',
|
|
132
|
+
'names those modifiers joined by + ahead of one character or a named key:',
|
|
133
|
+
'Enter, Tab, Escape, Backspace, Delete, Insert, Space, Home, End, PageUp,',
|
|
134
|
+
'PageDown, the four arrows as ArrowUp and the like, and F1 to F12. A key',
|
|
135
|
+
'moves no pointer, so the camera neither aims at it nor ends a zoom.',
|
|
136
|
+
'',
|
|
137
|
+
'drag drives pointer events, the kind a canvas board listens for. It',
|
|
138
|
+
'starts no native HTML5 drag of a draggable element on the stepped route,',
|
|
139
|
+
'so a beat needing one films the same only on the screencast route.',
|
|
140
|
+
'',
|
|
141
|
+
'A top-level route sets the route of every beat that names none of its',
|
|
142
|
+
'own. A beat carrying push: false keeps its camera on the whole page,',
|
|
143
|
+
'with the cursor and click ring still in the track, and its log.json',
|
|
144
|
+
'records push: false. Each log.json carries clip, the clip size in',
|
|
145
|
+
'pixels, which caps the zoom depth so a 1920-wide screencast stays sharp.',
|
|
146
|
+
'',
|
|
147
|
+
'A target is a CSS selector, or text=<words> for the first innermost',
|
|
148
|
+
'element with a size whose whole text is those words, so a hidden copy',
|
|
149
|
+
'earlier in the page is skipped. Setup steps run before the first kept',
|
|
115
150
|
'frame. Every beat opens and closes on a 0.6 s hold.',
|
|
116
151
|
'',
|
|
117
152
|
'Needs a reachable page, FFmpeg, and the headless shell. Install with:',
|
|
@@ -123,7 +158,8 @@ export function register(program: Command): void {
|
|
|
123
158
|
' no-beats the plan names no beat',
|
|
124
159
|
' bad-beat a beat has no name or a setup that is not a list',
|
|
125
160
|
' no-steps a beat names no step',
|
|
126
|
-
' unknown-route a beat names a route other than step or
|
|
161
|
+
' unknown-route the plan or a beat names a route other than step or',
|
|
162
|
+
' screencast',
|
|
127
163
|
' bad-step a step is missing what the take needs to perform it',
|
|
128
164
|
' engine-missing the browser engine is not installed in this project',
|
|
129
165
|
' ffmpeg-missing FFmpeg is not on the PATH',
|
|
@@ -133,6 +169,13 @@ export function register(program: Command): void {
|
|
|
133
169
|
' take-failed the take failed against a reachable page',
|
|
134
170
|
' reel-failed every beat was filmed and the reel did not encode',
|
|
135
171
|
'',
|
|
172
|
+
'A refusal in a beat carries beat. target-missing carries still when the',
|
|
173
|
+
"page answered a capture, the beat folder's still.jpg as the step met it. A",
|
|
174
|
+
'beat that fell back to the screencast before refusing carries fallback,',
|
|
175
|
+
'the reason it fell back. The screencast opens the address a second time,',
|
|
176
|
+
'so a single-use link is spent by then: check the access before the',
|
|
177
|
+
'selector.',
|
|
178
|
+
'',
|
|
136
179
|
'Exit codes:',
|
|
137
180
|
' 0 every beat was filmed, and the reel written when asked for',
|
|
138
181
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
@@ -144,15 +187,113 @@ export function register(program: Command): void {
|
|
|
144
187
|
'',
|
|
145
188
|
].join('\n'),
|
|
146
189
|
)
|
|
147
|
-
.action(
|
|
148
|
-
|
|
190
|
+
.action(
|
|
191
|
+
async (url: string, plan: string, opts: RunOptions, command: Command) => {
|
|
192
|
+
if (!opts.out)
|
|
193
|
+
command.error("error: required option '--out <dir>' not specified")
|
|
194
|
+
process.exitCode = await runTake(url, plan, { ...opts, out: opts.out })
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
take
|
|
199
|
+
.command('camera')
|
|
200
|
+
.description("Build a camera track from a take's log.json")
|
|
201
|
+
.argument('<log>', "A beat's log.json, as a take wrote it")
|
|
202
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
203
|
+
.option('--out <file>', 'Write the track to a file rather than stdout')
|
|
204
|
+
.option(
|
|
205
|
+
'--rate <n>',
|
|
206
|
+
'How many times faster the edit plays the clip, 1 by default',
|
|
207
|
+
)
|
|
208
|
+
.option('--json', 'Add a machine-readable record on stdout with --out')
|
|
209
|
+
.addHelpText(
|
|
210
|
+
'after',
|
|
211
|
+
[
|
|
212
|
+
'',
|
|
213
|
+
"Builds the same camera track a take writes beside each beat's clip, from",
|
|
214
|
+
'the log alone, so a track can be rebuilt for a clip the edit plays at',
|
|
215
|
+
'another speed. With no --out the track is the data on stdout, one line,',
|
|
216
|
+
'and a refusal under --json is the record there instead.',
|
|
217
|
+
'',
|
|
218
|
+
'--rate <n> builds the track on the playback clock of a clip sped up n',
|
|
219
|
+
'times, dividing every time in the log by n, so a zoom settles at the',
|
|
220
|
+
'speed a viewer sees rather than pumping several times a beat.',
|
|
221
|
+
'',
|
|
222
|
+
"The zoom depth is capped by the clip's resolution, read from the log's",
|
|
223
|
+
'clip field, or from its route on a log written before that field. A log',
|
|
224
|
+
'carrying push: false holds the whole page with the cursor and ring.',
|
|
225
|
+
'',
|
|
226
|
+
'Reasons:',
|
|
227
|
+
' no-log-file nothing readable at the log path',
|
|
228
|
+
" unreadable-log the file is not a take's log.json",
|
|
229
|
+
' bad-rate --rate is not a positive number',
|
|
230
|
+
'',
|
|
231
|
+
'Exit codes:',
|
|
232
|
+
' 0 the track was written',
|
|
233
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
234
|
+
'',
|
|
235
|
+
'Examples:',
|
|
236
|
+
" canon take camera takes/01-open/log.json | jq '.frames | length'",
|
|
237
|
+
' canon take camera takes/01-open/log.json --rate 2 --out camera-2x.json',
|
|
238
|
+
'',
|
|
239
|
+
].join('\n'),
|
|
240
|
+
)
|
|
241
|
+
.action((log: string, opts: CameraOptions) => {
|
|
242
|
+
process.exitCode = runCamera(log, opts)
|
|
149
243
|
})
|
|
150
244
|
}
|
|
151
245
|
|
|
246
|
+
function runCamera(logPath: string, opts: CameraOptions): number {
|
|
247
|
+
const emitJson = opts.json ?? false
|
|
248
|
+
const path = resolve(logPath)
|
|
249
|
+
const refuseCamera = (reason: string, message: string): number => {
|
|
250
|
+
logWarn(message)
|
|
251
|
+
emit(emitJson, { log: path, reason, message })
|
|
252
|
+
return 1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const rate = opts.rate === undefined ? 1 : readRate(opts.rate)
|
|
256
|
+
if (rate === undefined) {
|
|
257
|
+
return refuseCamera(
|
|
258
|
+
'bad-rate',
|
|
259
|
+
`--rate ${opts.rate} is not a positive number, such as 1.5 or 2.`,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let source: string
|
|
264
|
+
try {
|
|
265
|
+
source = readFileSync(path, 'utf8')
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return refuseCamera(
|
|
268
|
+
'no-log-file',
|
|
269
|
+
`No log file at ${path}. ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
const read = readTakeLog(source)
|
|
273
|
+
if (read.kind === 'refused') {
|
|
274
|
+
return refuseCamera(
|
|
275
|
+
'unreadable-log',
|
|
276
|
+
`${path} is not a take's log.json, being the file beside a beat's clip.mp4. ${read.detail}`,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const track = cameraTrack(read.log, { rate })
|
|
281
|
+
const line = `${JSON.stringify(track)}\n`
|
|
282
|
+
if (!opts.out) {
|
|
283
|
+
process.stdout.write(line)
|
|
284
|
+
return 0
|
|
285
|
+
}
|
|
286
|
+
const out = resolve(opts.out)
|
|
287
|
+
writeFileSync(out, line)
|
|
288
|
+
logInfo(`${out} ${plural(track.frames.length, 'frame')} rate ${rate}`)
|
|
289
|
+
emit(emitJson, { log: path, out, frames: track.frames.length, rate })
|
|
290
|
+
return 0
|
|
291
|
+
}
|
|
292
|
+
|
|
152
293
|
async function runTake(
|
|
153
294
|
url: string,
|
|
154
295
|
planPath: string,
|
|
155
|
-
opts: RunOptions,
|
|
296
|
+
opts: RunOptions & { readonly out: string },
|
|
156
297
|
): Promise<number> {
|
|
157
298
|
const emitJson = opts.json ?? false
|
|
158
299
|
const path = resolve(planPath)
|
|
@@ -209,6 +350,11 @@ async function runTake(
|
|
|
209
350
|
logStep('Refused')
|
|
210
351
|
logWarn(TAKE_REFUSALS[result.reason])
|
|
211
352
|
if (result.beat) logWarn(`in beat ${result.beat}`)
|
|
353
|
+
if (result.fallback) {
|
|
354
|
+
logWarn(
|
|
355
|
+
`after falling back to a screencast, which opened the address again: ${result.fallback}`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
212
358
|
if (result.reason === 'server-unreachable') {
|
|
213
359
|
logWarn(`Start whatever serves ${url}, then run this again.`)
|
|
214
360
|
}
|
|
@@ -224,6 +370,8 @@ async function runTake(
|
|
|
224
370
|
reason: result.reason,
|
|
225
371
|
message: result.message,
|
|
226
372
|
...(result.beat ? { beat: result.beat } : {}),
|
|
373
|
+
...(result.fallback ? { fallback: result.fallback } : {}),
|
|
374
|
+
...(result.still ? { still: result.still } : {}),
|
|
227
375
|
...(result.reason === 'browser-missing'
|
|
228
376
|
? { install: INSTALL_BROWSER }
|
|
229
377
|
: {}),
|
package/src/commands/tasks.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { Command } from 'commander'
|
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { gitEnv } from '@/git/env'
|
|
5
5
|
import { type AnswersOutcome, planAnswers } from '@/tasks/answers'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_LAUNCH_WAIT_SECONDS,
|
|
8
|
+
type LaunchOutcome,
|
|
9
|
+
type LaunchWait,
|
|
10
|
+
readLaunchable,
|
|
11
|
+
waitForLaunchable,
|
|
12
|
+
} from '@/tasks/launch-wait'
|
|
6
13
|
import {
|
|
7
14
|
type BoardOutcome,
|
|
8
15
|
type BoardRefused,
|
|
@@ -112,6 +119,12 @@ interface AnswersCommandOptions {
|
|
|
112
119
|
readonly root?: string
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
interface LaunchableCommandOptions {
|
|
123
|
+
readonly json?: boolean
|
|
124
|
+
readonly root?: string
|
|
125
|
+
readonly wait?: string | true
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
interface BranchCommandOptions {
|
|
116
129
|
readonly json?: boolean
|
|
117
130
|
readonly root?: string
|
|
@@ -414,6 +427,72 @@ export function register(program: Command): void {
|
|
|
414
427
|
process.exitCode = await runAnswers(plan, opts)
|
|
415
428
|
})
|
|
416
429
|
|
|
430
|
+
tasks
|
|
431
|
+
.command('launchable')
|
|
432
|
+
.description(
|
|
433
|
+
'Report whether a row has a linked plan the dispatch may launch',
|
|
434
|
+
)
|
|
435
|
+
.argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
|
|
436
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
437
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
438
|
+
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
439
|
+
.option(
|
|
440
|
+
'--wait [seconds]',
|
|
441
|
+
`Hold until the row is launchable, default ${DEFAULT_LAUNCH_WAIT_SECONDS}`,
|
|
442
|
+
)
|
|
443
|
+
.addHelpText(
|
|
444
|
+
'after',
|
|
445
|
+
[
|
|
446
|
+
'',
|
|
447
|
+
"Resolves the task's Plan: line and runs the plan-answers gate on the",
|
|
448
|
+
'plan it names, in one read. It keys on the line rather than on the',
|
|
449
|
+
'plan file, since a planner links a plan only after it validates, so a',
|
|
450
|
+
'plan still being written never reads as landed. It answers planning',
|
|
451
|
+
'alone: the branch and claim checks stay in the dispatch runbook.',
|
|
452
|
+
'',
|
|
453
|
+
'The record carries task, plan, launchable, and open, the last three',
|
|
454
|
+
'named as plan-answers names them. Read `reason`:',
|
|
455
|
+
' unlinked the task carries no Plan: line yet',
|
|
456
|
+
' open the plan still holds a question for the operator',
|
|
457
|
+
' launchable the plan waits on nobody',
|
|
458
|
+
'',
|
|
459
|
+
'`--wait [seconds]` re-reads every 30 seconds until the row is',
|
|
460
|
+
'launchable or the deadline passes. `reason` then reads `launchable`',
|
|
461
|
+
'or `timeout`, the record carries the last reading, and',
|
|
462
|
+
'`waitedSeconds` is added. A refusal ends the wait at once. A wait',
|
|
463
|
+
'past 600 seconds outlasts a foreground shell call, so run it in the',
|
|
464
|
+
'background and act on the wake.',
|
|
465
|
+
'',
|
|
466
|
+
'Refusals, read off `reason`:',
|
|
467
|
+
' no-board the project carries no .canon/tasks/',
|
|
468
|
+
' no-match no task carries that stem exactly',
|
|
469
|
+
' ambiguous several tasks start with the name, so pass one in full',
|
|
470
|
+
' several-plans the Plan: line links more than one plan',
|
|
471
|
+
' no-plan the Plan: line names no file',
|
|
472
|
+
' archived the Plan: line points into the plans archive',
|
|
473
|
+
' bad-input the Plan: line carries an empty target',
|
|
474
|
+
' bad-wait --wait takes a whole number of seconds above zero',
|
|
475
|
+
'',
|
|
476
|
+
'Exit codes:',
|
|
477
|
+
' 0 the row is launchable',
|
|
478
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
479
|
+
' 2 the row is unlinked or open, or the wait reached its deadline',
|
|
480
|
+
'',
|
|
481
|
+
'It reports and never writes. Branch on `reason` rather than on the',
|
|
482
|
+
'exit code, which a shell function wrapping canon can flatten to zero.',
|
|
483
|
+
'',
|
|
484
|
+
'Examples:',
|
|
485
|
+
' canon tasks launchable v28.1-trigger-escalation',
|
|
486
|
+
' canon tasks launchable v28.1-trigger-escalation --json',
|
|
487
|
+
' canon tasks launchable v28.1-trigger-escalation --wait --json',
|
|
488
|
+
' canon tasks launchable v28.1-trigger-escalation --wait 900 --json',
|
|
489
|
+
'',
|
|
490
|
+
].join('\n'),
|
|
491
|
+
)
|
|
492
|
+
.action(async (task: string, opts: LaunchableCommandOptions) => {
|
|
493
|
+
process.exitCode = await runLaunchable(task, opts)
|
|
494
|
+
})
|
|
495
|
+
|
|
417
496
|
tasks
|
|
418
497
|
.command('plan-branch')
|
|
419
498
|
.description('Derive the branch name a dispatch and a worker both take')
|
|
@@ -1827,6 +1906,110 @@ function reportAnswers(
|
|
|
1827
1906
|
return EXIT_FINDINGS
|
|
1828
1907
|
}
|
|
1829
1908
|
|
|
1909
|
+
const BAD_WAIT = '--wait takes a whole number of seconds above zero.'
|
|
1910
|
+
|
|
1911
|
+
interface BadWait {
|
|
1912
|
+
readonly ok: false
|
|
1913
|
+
readonly reason: 'bad-wait'
|
|
1914
|
+
readonly message: string
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function runLaunchable(
|
|
1918
|
+
task: string,
|
|
1919
|
+
opts: LaunchableCommandOptions,
|
|
1920
|
+
): Promise<number> {
|
|
1921
|
+
const emitJson = opts.json ?? false
|
|
1922
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
1923
|
+
|
|
1924
|
+
let waitSeconds: number | undefined
|
|
1925
|
+
if (opts.wait === true) {
|
|
1926
|
+
waitSeconds = DEFAULT_LAUNCH_WAIT_SECONDS
|
|
1927
|
+
} else if (opts.wait !== undefined) {
|
|
1928
|
+
if (!/^\d+$/.test(opts.wait) || Number(opts.wait) === 0) {
|
|
1929
|
+
const refused: BadWait = {
|
|
1930
|
+
ok: false,
|
|
1931
|
+
reason: 'bad-wait',
|
|
1932
|
+
message: BAD_WAIT,
|
|
1933
|
+
}
|
|
1934
|
+
return reportLaunchable(refused, emitJson, root)
|
|
1935
|
+
}
|
|
1936
|
+
waitSeconds = Number(opts.wait)
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const outcome =
|
|
1940
|
+
waitSeconds === undefined
|
|
1941
|
+
? await readLaunchable(root, task)
|
|
1942
|
+
: await waitForLaunchable(root, task, {
|
|
1943
|
+
waitSeconds,
|
|
1944
|
+
clock: {
|
|
1945
|
+
now: () => Date.now(),
|
|
1946
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
1947
|
+
},
|
|
1948
|
+
})
|
|
1949
|
+
|
|
1950
|
+
return reportLaunchable(outcome, emitJson, root)
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function reportLaunchable(
|
|
1954
|
+
outcome: LaunchOutcome | LaunchWait | BadWait,
|
|
1955
|
+
emitJson: boolean,
|
|
1956
|
+
root: string,
|
|
1957
|
+
): number {
|
|
1958
|
+
if (!outcome.ok) {
|
|
1959
|
+
intro('canon tasks launchable')
|
|
1960
|
+
logStep('Refused')
|
|
1961
|
+
logError(outcome.message)
|
|
1962
|
+
outro()
|
|
1963
|
+
|
|
1964
|
+
if (emitJson) {
|
|
1965
|
+
process.stdout.write(
|
|
1966
|
+
`${JSON.stringify({
|
|
1967
|
+
ok: false,
|
|
1968
|
+
reason: outcome.reason,
|
|
1969
|
+
message: outcome.message,
|
|
1970
|
+
...('waitedSeconds' in outcome && {
|
|
1971
|
+
waitedSeconds: outcome.waitedSeconds,
|
|
1972
|
+
}),
|
|
1973
|
+
root,
|
|
1974
|
+
})}\n`,
|
|
1975
|
+
)
|
|
1976
|
+
}
|
|
1977
|
+
return 1
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const code = outcome.launchable ? 0 : EXIT_FINDINGS
|
|
1981
|
+
|
|
1982
|
+
if (emitJson) {
|
|
1983
|
+
process.stdout.write(`${JSON.stringify({ ...outcome, root })}\n`)
|
|
1984
|
+
return code
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
intro('canon tasks launchable')
|
|
1988
|
+
logStep(outcome.task)
|
|
1989
|
+
|
|
1990
|
+
if (outcome.plan === undefined) {
|
|
1991
|
+
logWarn('carries no Plan: line yet, so no plan has landed.')
|
|
1992
|
+
} else if (outcome.launchable) {
|
|
1993
|
+
logInfo(`${outcome.plan} waits on nobody, so the dispatch may launch it.`)
|
|
1994
|
+
} else {
|
|
1995
|
+
for (const question of outcome.open) {
|
|
1996
|
+
logWarn(`${question.label} ${question.why}`)
|
|
1997
|
+
}
|
|
1998
|
+
logError(`${outcome.plan} still waits on the operator.`)
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if ('waitedSeconds' in outcome) {
|
|
2002
|
+
logInfo(
|
|
2003
|
+
outcome.reason === 'timeout'
|
|
2004
|
+
? `not launchable within ${outcome.waitedSeconds}s`
|
|
2005
|
+
: `launchable after ${outcome.waitedSeconds}s`,
|
|
2006
|
+
)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
outro()
|
|
2010
|
+
return code
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1830
2013
|
async function runBranch(
|
|
1831
2014
|
plan: string,
|
|
1832
2015
|
opts: BranchCommandOptions,
|