@erclx/canon 5.27.0 → 5.29.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/draft-ready/SKILL.md +3 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +4 -2
- package/claude/skills/review-address/SKILL.md +16 -7
- package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
- package/claude/skills/review-craft/references/ui.md +4 -4
- package/claude/skills/review-ui/REQUIREMENT.md +2 -2
- package/claude/skills/review-ui/SKILL.md +13 -11
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +19 -7
- package/claude/skills/review-ui/references/tick.md +22 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
- package/claude/skills/role-orchestrator/SKILL.md +24 -26
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +12 -2
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
- package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +3 -2
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/docs/agents/canvas.md +12 -4
- package/docs/agents/gate.md +3 -1
- package/docs/agents/index.md +2 -1
- package/docs/agents/pr-evidence.md +15 -14
- package/docs/agents/pr-tick.md +72 -0
- package/docs/agents/sandbox.md +2 -2
- package/docs/agents/tasks-plans.md +1 -1
- package/docs/agents/tasks-validate.md +18 -18
- package/docs/agents/tasks.md +1 -1
- package/docs/workflow/operating-model.md +10 -15
- package/docs/workflow/parallel-features.md +7 -7
- package/package.json +1 -1
- package/src/audits/baseline.ts +1 -1
- package/src/audits/run.ts +1 -1
- package/src/canvas/capture.ts +1 -1
- package/src/canvas/client/app.tsx +105 -4
- package/src/canvas/client/inspector/field.tsx +61 -2
- package/src/canvas/client/inspector/fields.css +78 -0
- package/src/canvas/client/inspector/token-select.tsx +109 -0
- package/src/canvas/client/inspector/values.ts +74 -2
- package/src/canvas/client/inspector.tsx +134 -15
- package/src/canvas/client/keys.ts +71 -0
- package/src/canvas/client/pages-panel.tsx +5 -1
- package/src/canvas/client/panel-handle.tsx +90 -0
- package/src/canvas/client/selection.tsx +3 -1
- package/src/canvas/client/shell.css +194 -4
- package/src/canvas/client/state.ts +255 -11
- package/src/canvas/client/surface.tsx +231 -18
- package/src/canvas/client/tools.tsx +153 -0
- package/src/canvas/content.ts +66 -1
- package/src/canvas/edit.ts +207 -25
- package/src/canvas/history.ts +116 -0
- package/src/canvas/server.ts +119 -13
- package/src/canvas/tokens.ts +1 -1
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-reach.ts +1 -1
- package/src/{exec.ts → cli/exec.ts} +2 -2
- package/src/{help.ts → cli/help.ts} +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/audits.ts +9 -1
- package/src/commands/autoship.ts +1 -1
- package/src/commands/canvas.ts +2 -2
- package/src/commands/capture.ts +1 -1
- package/src/commands/census.ts +1 -1
- package/src/commands/claude.ts +5 -5
- package/src/commands/comments.ts +1 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/demo.ts +9 -1
- package/src/commands/deps.ts +1 -1
- package/src/commands/design.ts +12 -4
- package/src/commands/docs.ts +2 -2
- package/src/commands/driver.ts +9 -1
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gate.ts +10 -3
- package/src/commands/gov.ts +3 -3
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +11 -3
- package/src/commands/intake.ts +1 -1
- package/src/commands/inventory.ts +9 -1
- package/src/commands/labels.ts +1 -1
- package/src/commands/markdown.ts +1 -1
- package/src/commands/migrate.ts +2 -2
- package/src/commands/pr.ts +324 -9
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +11 -14
- package/src/commands/secrets.ts +9 -1
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +2 -2
- package/src/commands/standards.ts +1 -1
- package/src/commands/sync.ts +3 -3
- package/src/commands/targets.ts +1 -1
- package/src/commands/tasks.ts +20 -3
- package/src/commands/teach.ts +1 -1
- package/src/commands/tooling.ts +2 -2
- package/src/commands/transcripts.ts +2 -2
- package/src/commands/upgrade.ts +2 -2
- package/src/commands/wiki.ts +2 -2
- package/src/commands/worktrees.ts +1 -1
- package/src/context/architecture.ts +1 -1
- package/src/context/citations.ts +2 -2
- package/src/context/classify/extract.ts +1 -1
- package/src/context/folders.ts +2 -2
- package/src/context/requirements.ts +1 -1
- package/src/docs/read.ts +1 -1
- package/src/feedback/github.ts +1 -1
- package/src/gate/measures.ts +2 -2
- package/src/gate/sequencer.ts +55 -3
- package/src/gate/stages.ts +8 -0
- package/src/gate/suite-lock.ts +214 -0
- package/src/init/run.ts +1 -1
- package/src/intake/folder.ts +1 -1
- package/src/labels/map.ts +1 -1
- package/src/labels/phase.ts +3 -3
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/records.ts +11 -11
- package/src/migrate/rename.ts +1 -1
- package/src/migrate/scratch-evidence.ts +1 -1
- package/src/migrate/surface-roots.ts +6 -6
- package/src/pr/evidence.ts +188 -16
- package/src/pr/tick.ts +56 -0
- package/src/process/harness.ts +1 -1
- package/src/records/backup.ts +1 -1
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +1 -1
- package/src/records/size.ts +1 -1
- package/src/records/stale-canonical.ts +1 -1
- package/src/records/stale.ts +1 -1
- package/src/records/validate.ts +2 -2
- package/src/{project-root.ts → roots/project.ts} +2 -2
- package/src/{record-root.ts → roots/record.ts} +1 -1
- package/src/sandbox/coverage.ts +11 -12
- package/src/sandbox/equivalence.ts +57 -26
- package/src/sandbox/headless/run.ts +1 -1
- package/src/sandbox/hooks.ts +1 -1
- package/src/sandbox/provision.ts +79 -29
- package/src/sandbox/scenario.ts +588 -0
- package/src/serve/report.ts +1 -1
- package/src/sessions/live.ts +4 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/slides/decks.ts +1 -1
- package/src/standards/read.ts +1 -1
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/layout.ts +2 -2
- package/src/sync/stamp.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/validate.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/label.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/validate.ts +38 -14
- package/src/teach/nav.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/read.ts +1 -1
- package/src/tooling/scan.ts +1 -1
- package/src/tooling/verify-stack.ts +1 -1
- package/src/version/installed.ts +1 -1
- package/src/version/skew.ts +1 -1
- package/standards/board.md +3 -3
- package/standards/plan.md +13 -7
- package/tsconfig.json +1 -1
- /package/src/{cli-run.ts → cli/run.ts} +0 -0
- /package/src/{ui.ts → cli/ui.ts} +0 -0
- /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
- /package/src/{surface-root.ts → roots/surface.ts} +0 -0
package/src/commands/pr.ts
CHANGED
|
@@ -29,12 +29,19 @@ import {
|
|
|
29
29
|
findEvidenceCaseCount,
|
|
30
30
|
findEvidenceChecklist,
|
|
31
31
|
findEvidenceCommentId,
|
|
32
|
+
findEvidenceHead,
|
|
32
33
|
findEvidenceLocal,
|
|
33
34
|
findEvidencePreview,
|
|
35
|
+
type EvidenceItem,
|
|
36
|
+
type EvidenceReading,
|
|
34
37
|
groupEvidence,
|
|
35
38
|
hasMarkedEvidenceComment,
|
|
39
|
+
PNG_HEADER_BYTES,
|
|
40
|
+
readChecklistBoxes,
|
|
36
41
|
readOwed,
|
|
42
|
+
readPngWidth,
|
|
37
43
|
renderEvidenceBody,
|
|
44
|
+
settleChecklist,
|
|
38
45
|
} from '@/pr/evidence'
|
|
39
46
|
import {
|
|
40
47
|
dropFrames,
|
|
@@ -71,6 +78,7 @@ import {
|
|
|
71
78
|
servesChange,
|
|
72
79
|
type WorkflowFile,
|
|
73
80
|
} from '@/pr/preview'
|
|
81
|
+
import { type TickRefusal, tickBoxes } from '@/pr/tick'
|
|
74
82
|
import {
|
|
75
83
|
commentRowOf,
|
|
76
84
|
identityOf,
|
|
@@ -83,7 +91,7 @@ import {
|
|
|
83
91
|
selectBranchPull,
|
|
84
92
|
} from '@/pr/rest'
|
|
85
93
|
import { resolveReviewScope } from '@/pr/review-scope'
|
|
86
|
-
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
94
|
+
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
|
|
87
95
|
|
|
88
96
|
const GH_TIMEOUT_MS = 30_000
|
|
89
97
|
|
|
@@ -113,6 +121,28 @@ interface LocalOptions extends ReadOptions {
|
|
|
113
121
|
readonly remove?: boolean
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
interface TickOptions extends ReadOptions {
|
|
125
|
+
readonly boxes?: string
|
|
126
|
+
readonly head?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type TickReason = TickRefusal | 'stale-head' | 'bad-boxes' | 'no-comment'
|
|
130
|
+
|
|
131
|
+
const TICK_REFUSALS: Record<TickReason | 'gh-failed', string> = {
|
|
132
|
+
'stale-head':
|
|
133
|
+
'The head named is not the branch tip on the remote, so a tick would claim a pass on a commit that is no longer the pull request. Re-drive the checklist at the tip.',
|
|
134
|
+
'bad-boxes':
|
|
135
|
+
'--boxes takes box numbers as a comma-separated list of positive whole numbers, such as 1,3, and --head takes the commit that was driven.',
|
|
136
|
+
'no-comment': 'No comment on this pull request carries the evidence marker.',
|
|
137
|
+
'no-checklist': 'The evidence comment carries no checklist to tick.',
|
|
138
|
+
'no-box':
|
|
139
|
+
'A named box is past the end of the checklist. Number boxes off `canon pr evidence --json`.',
|
|
140
|
+
'taste-box':
|
|
141
|
+
'A named box ends in (taste), which no driver passes. Nothing was written.',
|
|
142
|
+
'gh-failed':
|
|
143
|
+
'gh could not read or edit the marked comment on this pull request.',
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/** Shared by every verb that resolves a pull request from the checkout's branch. */
|
|
117
147
|
const AMBIGUOUS_PULL =
|
|
118
148
|
'More than one pull request is open on this branch, each against another base. Name the pull request number.'
|
|
@@ -563,10 +593,12 @@ export function register(program: Command): void {
|
|
|
563
593
|
'--checklist closes the body with a visual checklist, below the',
|
|
564
594
|
'comparison it annotates. A checklist the marked comment already',
|
|
565
595
|
'carries is read back and carried forward the same way the preview',
|
|
566
|
-
'address is
|
|
567
|
-
'
|
|
568
|
-
'
|
|
569
|
-
'
|
|
596
|
+
'address is. A tick survives only a render at the head it names: one',
|
|
597
|
+
'stamped `passed at <sha>` for another head is cleared and unstamped,',
|
|
598
|
+
'and so is one carrying no stamp when the comment described another',
|
|
599
|
+
'head. A checklist, supplied or carried, turns no-evidence into ok: a',
|
|
600
|
+
'branch with a checklist and no evidence image renders a marked body',
|
|
601
|
+
'holding the checklist, so a later call finds and edits that comment.',
|
|
570
602
|
'',
|
|
571
603
|
'--local adds a **Local preview:** segment after any hosted one on the',
|
|
572
604
|
'address line, or carries the line alone, and is carried forward the',
|
|
@@ -580,6 +612,8 @@ export function register(program: Command): void {
|
|
|
580
612
|
' preview the hosted **Preview:** address the comment carries',
|
|
581
613
|
' local the **Local preview:** address beside it',
|
|
582
614
|
' checklist the checklist between its delimiters, ticks included',
|
|
615
|
+
' boxes each checklist box with its number, tick, stamp, and taste',
|
|
616
|
+
' mark, the numbering `canon pr tick` takes',
|
|
583
617
|
'These come from the comment already posted, never from the flags this',
|
|
584
618
|
'call passed. A checklist the caller posted raw after a refused render',
|
|
585
619
|
'carries no marker, so the record reports none of the three for it. An',
|
|
@@ -741,6 +775,51 @@ export function register(program: Command): void {
|
|
|
741
775
|
: await runLocal(opts)
|
|
742
776
|
})
|
|
743
777
|
|
|
778
|
+
pr.command('tick')
|
|
779
|
+
.description(
|
|
780
|
+
"Tick the checklist boxes a UI pass drove, stamping each with the commit it passed at, on the pull request's evidence comment",
|
|
781
|
+
)
|
|
782
|
+
.argument('[number]', 'Pull request to edit, defaulting to this branch')
|
|
783
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
784
|
+
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
785
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
786
|
+
.requiredOption(
|
|
787
|
+
'--boxes <list>',
|
|
788
|
+
'Box numbers to tick, comma-separated, as `canon pr evidence --json` numbers them',
|
|
789
|
+
)
|
|
790
|
+
.requiredOption('--head <sha>', 'The commit the boxes were driven at')
|
|
791
|
+
.addHelpText(
|
|
792
|
+
'after',
|
|
793
|
+
[
|
|
794
|
+
'',
|
|
795
|
+
'Edits the marked evidence comment in place, so a skill never',
|
|
796
|
+
'hand-edits a comment body. Each named box becomes',
|
|
797
|
+
'`- [x] <box> · passed at <short sha>`. Ticking a box again restamps',
|
|
798
|
+
'it. A later render at another head clears a tick whose stamp names',
|
|
799
|
+
'a different commit.',
|
|
800
|
+
'',
|
|
801
|
+
'Read `reason` on the JSON record:',
|
|
802
|
+
' ticked the boxes were ticked, with the tip in `head`',
|
|
803
|
+
' stale-head --head is not the remote branch tip, nothing written',
|
|
804
|
+
' taste-box a named box ends in (taste), nothing written',
|
|
805
|
+
' no-box a named number is past the checklist, nothing written',
|
|
806
|
+
' no-checklist the evidence comment carries no checklist',
|
|
807
|
+
' no-comment no comment carries the evidence marker',
|
|
808
|
+
' bad-boxes --boxes or --head is malformed',
|
|
809
|
+
'',
|
|
810
|
+
'Exit codes:',
|
|
811
|
+
' 0 the boxes were ticked',
|
|
812
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
813
|
+
'',
|
|
814
|
+
'Examples:',
|
|
815
|
+
' canon pr tick 1341 --boxes 1,2,4 --head 1a2b3c4 --json',
|
|
816
|
+
'',
|
|
817
|
+
].join('\n'),
|
|
818
|
+
)
|
|
819
|
+
.action(async (number: string | undefined, opts: TickOptions) => {
|
|
820
|
+
process.exitCode = await runTick(number, opts)
|
|
821
|
+
})
|
|
822
|
+
|
|
744
823
|
pr.command('frames')
|
|
745
824
|
.description(
|
|
746
825
|
`Push a UI review frame to the never-merged ${FRAMES_BRANCH} branch and return its embed link, or drop frames from it`,
|
|
@@ -1671,6 +1750,134 @@ async function readPullMergeBase(
|
|
|
1671
1750
|
return sha === null || sha.trim() === '' ? undefined : sha.trim()
|
|
1672
1751
|
}
|
|
1673
1752
|
|
|
1753
|
+
/** How many width reads run at once, since each downloads a whole image. */
|
|
1754
|
+
const WIDTH_READ_CONCURRENCY = 4
|
|
1755
|
+
|
|
1756
|
+
type WidthRead =
|
|
1757
|
+
| { readonly kind: 'read'; readonly width?: number }
|
|
1758
|
+
| { readonly kind: 'failed' }
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* The pixel width of a PNG at `sha`, read over the contents API like every
|
|
1762
|
+
* other evidence read. The endpoint ignores a `Range` header, so the whole
|
|
1763
|
+
* file arrives and only its first bytes are read. A format other than PNG
|
|
1764
|
+
* reads without a width, and a refused or timed-out call reads as failed, so
|
|
1765
|
+
* the caller can keep the ceiling width and still report the cause.
|
|
1766
|
+
*/
|
|
1767
|
+
async function readImageWidth(
|
|
1768
|
+
cwd: string,
|
|
1769
|
+
path: string,
|
|
1770
|
+
sha: string,
|
|
1771
|
+
): Promise<WidthRead> {
|
|
1772
|
+
if (!/\.png$/i.test(path)) return { kind: 'read' }
|
|
1773
|
+
try {
|
|
1774
|
+
const result = await execa(
|
|
1775
|
+
'gh',
|
|
1776
|
+
[
|
|
1777
|
+
'api',
|
|
1778
|
+
'-H',
|
|
1779
|
+
'Accept: application/vnd.github.raw',
|
|
1780
|
+
`repos/{owner}/{repo}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${sha}`,
|
|
1781
|
+
],
|
|
1782
|
+
{
|
|
1783
|
+
cwd,
|
|
1784
|
+
timeout: GH_TIMEOUT_MS,
|
|
1785
|
+
env: gitEnv(),
|
|
1786
|
+
extendEnv: false,
|
|
1787
|
+
encoding: 'buffer',
|
|
1788
|
+
},
|
|
1789
|
+
)
|
|
1790
|
+
const width = readPngWidth(result.stdout.subarray(0, PNG_HEADER_BYTES))
|
|
1791
|
+
return width === undefined ? { kind: 'read' } : { kind: 'read', width }
|
|
1792
|
+
} catch {
|
|
1793
|
+
return { kind: 'failed' }
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** Runs `task` over `inputs` with at most `limit` in flight, keeping input order. */
|
|
1798
|
+
async function mapPool<T, R>(
|
|
1799
|
+
inputs: readonly T[],
|
|
1800
|
+
limit: number,
|
|
1801
|
+
task: (input: T) => Promise<R>,
|
|
1802
|
+
): Promise<R[]> {
|
|
1803
|
+
const results: R[] = new Array(inputs.length)
|
|
1804
|
+
let next = 0
|
|
1805
|
+
const lane = async (): Promise<void> => {
|
|
1806
|
+
while (next < inputs.length) {
|
|
1807
|
+
const index = next++
|
|
1808
|
+
results[index] = await task(inputs[index] as T)
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
await Promise.all(
|
|
1812
|
+
Array.from({ length: Math.min(limit, inputs.length) }, lane),
|
|
1813
|
+
)
|
|
1814
|
+
return results
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
interface SizedEvidence {
|
|
1818
|
+
readonly reading: EvidenceReading
|
|
1819
|
+
/** Each image whose width read failed, as `head:<path>` or `base:<path>`, which rendered at the ceiling. */
|
|
1820
|
+
readonly unread: readonly string[]
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/** Each item with the pixel width of its head image and, when it has one, its base image. */
|
|
1824
|
+
async function withImageWidths(
|
|
1825
|
+
cwd: string,
|
|
1826
|
+
reading: EvidenceReading,
|
|
1827
|
+
base: string | undefined,
|
|
1828
|
+
head: string,
|
|
1829
|
+
): Promise<SizedEvidence> {
|
|
1830
|
+
if (reading.kind === 'refused') return { reading, unread: [] }
|
|
1831
|
+
const jobs = reading.states.flatMap((entry, stateIndex) =>
|
|
1832
|
+
entry.items.flatMap((item, itemIndex) => [
|
|
1833
|
+
{
|
|
1834
|
+
stateIndex,
|
|
1835
|
+
itemIndex,
|
|
1836
|
+
side: 'head' as const,
|
|
1837
|
+
path: item.path,
|
|
1838
|
+
sha: head,
|
|
1839
|
+
},
|
|
1840
|
+
...(item.added || base === undefined
|
|
1841
|
+
? []
|
|
1842
|
+
: [
|
|
1843
|
+
{
|
|
1844
|
+
stateIndex,
|
|
1845
|
+
itemIndex,
|
|
1846
|
+
side: 'base' as const,
|
|
1847
|
+
path: item.path,
|
|
1848
|
+
sha: base,
|
|
1849
|
+
},
|
|
1850
|
+
]),
|
|
1851
|
+
]),
|
|
1852
|
+
)
|
|
1853
|
+
const reads = await mapPool(jobs, WIDTH_READ_CONCURRENCY, (job) =>
|
|
1854
|
+
readImageWidth(cwd, job.path, job.sha),
|
|
1855
|
+
)
|
|
1856
|
+
const unread: string[] = []
|
|
1857
|
+
const states = reading.states.map((entry, stateIndex) => ({
|
|
1858
|
+
...entry,
|
|
1859
|
+
items: entry.items.map((item, itemIndex): EvidenceItem => {
|
|
1860
|
+
const widths: { width?: number; baseWidth?: number } = {}
|
|
1861
|
+
jobs.forEach((job, index) => {
|
|
1862
|
+
const read = reads[index]
|
|
1863
|
+
if (
|
|
1864
|
+
job.stateIndex !== stateIndex ||
|
|
1865
|
+
job.itemIndex !== itemIndex ||
|
|
1866
|
+
read === undefined
|
|
1867
|
+
) {
|
|
1868
|
+
return
|
|
1869
|
+
}
|
|
1870
|
+
if (read.kind === 'failed') unread.push(`${job.side}:${job.path}`)
|
|
1871
|
+
else if (read.width !== undefined) {
|
|
1872
|
+
widths[job.side === 'head' ? 'width' : 'baseWidth'] = read.width
|
|
1873
|
+
}
|
|
1874
|
+
})
|
|
1875
|
+
return { ...item, ...widths }
|
|
1876
|
+
}),
|
|
1877
|
+
}))
|
|
1878
|
+
return { reading: { kind: 'read', states }, unread }
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1674
1881
|
async function runEvidence(
|
|
1675
1882
|
number: string | undefined,
|
|
1676
1883
|
opts: EvidenceOptions,
|
|
@@ -1786,7 +1993,10 @@ async function runEvidence(
|
|
|
1786
1993
|
const carried = {
|
|
1787
1994
|
...(carriedPreview !== undefined && { preview: carriedPreview }),
|
|
1788
1995
|
...(carriedLocal !== undefined && { local: carriedLocal }),
|
|
1789
|
-
...(carriedChecklist !== undefined && {
|
|
1996
|
+
...(carriedChecklist !== undefined && {
|
|
1997
|
+
checklist: carriedChecklist,
|
|
1998
|
+
boxes: readChecklistBoxes(carriedChecklist),
|
|
1999
|
+
}),
|
|
1790
2000
|
}
|
|
1791
2001
|
|
|
1792
2002
|
// A deploy that resolves and does not build from any changed path makes a
|
|
@@ -1834,7 +2044,8 @@ async function runEvidence(
|
|
|
1834
2044
|
// comment a later call edits. A local address rides on a checklist or a
|
|
1835
2045
|
// hosted preview and never opens one alone, so a docs-only branch with a
|
|
1836
2046
|
// server up posts nothing.
|
|
1837
|
-
const hasChecklist =
|
|
2047
|
+
const hasChecklist =
|
|
2048
|
+
suppliedChecklist !== undefined || carriedChecklist !== undefined
|
|
1838
2049
|
if (
|
|
1839
2050
|
grouped.kind === 'refused' &&
|
|
1840
2051
|
opts.preview === undefined &&
|
|
@@ -1870,8 +2081,13 @@ async function runEvidence(
|
|
|
1870
2081
|
|
|
1871
2082
|
const preview = opts.preview ?? (isUnserved ? undefined : carriedPreview)
|
|
1872
2083
|
const local = opts.local ?? carriedLocal
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
2084
|
+
const unsettled = suppliedChecklist ?? carriedChecklist
|
|
2085
|
+
const checklist =
|
|
2086
|
+
unsettled === undefined
|
|
2087
|
+
? undefined
|
|
2088
|
+
: settleChecklist(unsettled, identity.head, findEvidenceHead(comments))
|
|
2089
|
+
const sized = await withImageWidths(root, grouped, base, identity.head)
|
|
2090
|
+
const states = sized.reading.kind === 'read' ? sized.reading.states : []
|
|
1875
2091
|
const body = renderEvidenceBody(
|
|
1876
2092
|
states,
|
|
1877
2093
|
repo,
|
|
@@ -1910,6 +2126,11 @@ async function runEvidence(
|
|
|
1910
2126
|
logInfo(
|
|
1911
2127
|
`${plural(caseCount, 'case')} across ${plural(states.length, 'state')}`,
|
|
1912
2128
|
)
|
|
2129
|
+
if (sized.unread.length > 0) {
|
|
2130
|
+
logWarn(
|
|
2131
|
+
`width unread for ${plural(sized.unread.length, 'image')}, rendered at 1000: ${sized.unread.join(', ')}`,
|
|
2132
|
+
)
|
|
2133
|
+
}
|
|
1913
2134
|
if (preview !== undefined) logInfo(`preview ${preview}`)
|
|
1914
2135
|
if (local !== undefined) logInfo(`local preview ${local}`)
|
|
1915
2136
|
if (checklist !== undefined) {
|
|
@@ -1932,6 +2153,7 @@ async function runEvidence(
|
|
|
1932
2153
|
head: identity.head,
|
|
1933
2154
|
body,
|
|
1934
2155
|
states,
|
|
2156
|
+
...(sized.unread.length > 0 && { widthUnread: sized.unread }),
|
|
1935
2157
|
...(commentId !== undefined && { commentId }),
|
|
1936
2158
|
...carried,
|
|
1937
2159
|
})}\n`,
|
|
@@ -2402,6 +2624,99 @@ async function runLocalRemove(
|
|
|
2402
2624
|
|
|
2403
2625
|
const POSITIVE_WHOLE = /^[1-9]\d*$/
|
|
2404
2626
|
|
|
2627
|
+
const MIN_TICK_HEAD_LENGTH = 7
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* Ticks the named boxes on the marked comment. It refuses a head that is not
|
|
2631
|
+
* the remote tip and a taste box before writing, so both rules hold as checks
|
|
2632
|
+
* rather than as prose a driver can talk itself out of.
|
|
2633
|
+
*/
|
|
2634
|
+
async function runTick(
|
|
2635
|
+
number: string | undefined,
|
|
2636
|
+
opts: TickOptions,
|
|
2637
|
+
): Promise<number> {
|
|
2638
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
2639
|
+
const emitJson = opts.json ?? false
|
|
2640
|
+
|
|
2641
|
+
intro('canon pr tick')
|
|
2642
|
+
|
|
2643
|
+
const refuseTick = (reason: TickReason | 'gh-failed'): number =>
|
|
2644
|
+
refuseWith(reason, TICK_REFUSALS[reason], emitJson, root)
|
|
2645
|
+
|
|
2646
|
+
const numbers = (opts.boxes ?? '').split(',').map((part) => part.trim())
|
|
2647
|
+
const head = (opts.head ?? '').trim()
|
|
2648
|
+
if (
|
|
2649
|
+
numbers.length === 0 ||
|
|
2650
|
+
!numbers.every((part) => POSITIVE_WHOLE.test(part)) ||
|
|
2651
|
+
!/^[0-9a-f]+$/.test(head) ||
|
|
2652
|
+
head.length < MIN_TICK_HEAD_LENGTH
|
|
2653
|
+
) {
|
|
2654
|
+
return refuseTick('bad-boxes')
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
const read = await readIdentity(root, number)
|
|
2658
|
+
if (read.kind === 'refused') {
|
|
2659
|
+
return refuseWith(read.reason, PULL_REFUSALS[read.reason], emitJson, root)
|
|
2660
|
+
}
|
|
2661
|
+
const { identity } = read
|
|
2662
|
+
if (identity.number === undefined) return refuseTick('gh-failed')
|
|
2663
|
+
|
|
2664
|
+
const reading = await resolveTip(identity.branch, refReader(root))
|
|
2665
|
+
if (reading.kind === 'refused') {
|
|
2666
|
+
return refuseWith(
|
|
2667
|
+
reading.reason,
|
|
2668
|
+
PULL_REFUSALS[reading.reason],
|
|
2669
|
+
emitJson,
|
|
2670
|
+
root,
|
|
2671
|
+
)
|
|
2672
|
+
}
|
|
2673
|
+
if (!reading.tip.startsWith(head)) return refuseTick('stale-head')
|
|
2674
|
+
|
|
2675
|
+
const comments = await listComments(root, String(identity.number))
|
|
2676
|
+
if (comments === undefined) return refuseTick('gh-failed')
|
|
2677
|
+
const commentId = findEvidenceCommentId(comments)
|
|
2678
|
+
const marked = comments.find((comment) =>
|
|
2679
|
+
comment.url?.endsWith(`#issuecomment-${commentId}`),
|
|
2680
|
+
)
|
|
2681
|
+
if (commentId === undefined || marked === undefined) {
|
|
2682
|
+
return refuseTick('no-comment')
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
const result = tickBoxes(marked.body, numbers.map(Number), reading.tip)
|
|
2686
|
+
if (result.kind === 'refused') return refuseTick(result.reason)
|
|
2687
|
+
|
|
2688
|
+
if (result.body !== marked.body) {
|
|
2689
|
+
const patched = await gh(root, [
|
|
2690
|
+
'api',
|
|
2691
|
+
'-X',
|
|
2692
|
+
'PATCH',
|
|
2693
|
+
`repos/{owner}/{repo}/issues/comments/${commentId}`,
|
|
2694
|
+
'-f',
|
|
2695
|
+
`body=${result.body}`,
|
|
2696
|
+
])
|
|
2697
|
+
if (patched === null) return refuseTick('gh-failed')
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
logStep('Ticked')
|
|
2701
|
+
logInfo(
|
|
2702
|
+
`${plural(numbers.length, 'box')} at ${reading.tip.slice(0, 7)} on comment ${commentId}`,
|
|
2703
|
+
)
|
|
2704
|
+
outro()
|
|
2705
|
+
if (emitJson) {
|
|
2706
|
+
process.stdout.write(
|
|
2707
|
+
`${JSON.stringify({
|
|
2708
|
+
root,
|
|
2709
|
+
number: identity.number,
|
|
2710
|
+
reason: 'ticked',
|
|
2711
|
+
head: reading.tip,
|
|
2712
|
+
boxes: numbers.map(Number),
|
|
2713
|
+
commentId,
|
|
2714
|
+
})}\n`,
|
|
2715
|
+
)
|
|
2716
|
+
}
|
|
2717
|
+
return 0
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2405
2720
|
/** One `gh api` call, keeping the HTTP status of a failure so a refused write can be told from a missing ref. */
|
|
2406
2721
|
type ApiCall =
|
|
2407
2722
|
| { readonly ok: true; readonly data: unknown }
|
package/src/commands/records.ts
CHANGED
package/src/commands/repo.ts
CHANGED
package/src/commands/sandbox.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
-
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project
|
|
4
|
+
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
5
5
|
import {
|
|
6
6
|
assertedPercent,
|
|
7
7
|
collectCensus,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from '@/sandbox/expect'
|
|
28
28
|
import { runHeadless } from '@/sandbox/headless/run'
|
|
29
29
|
import { runSandbox } from '@/sandbox/provision'
|
|
30
|
+
import { listCategories, listScenarioFiles } from '@/sandbox/scenario'
|
|
30
31
|
import { sandboxTree } from '@/sandbox/tree'
|
|
31
32
|
import {
|
|
32
33
|
frameError,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
outro,
|
|
40
41
|
palette,
|
|
41
42
|
select,
|
|
42
|
-
} from '@/ui'
|
|
43
|
+
} from '@/cli/ui'
|
|
43
44
|
|
|
44
45
|
const SANDBOX_DIR = join(PROJECT_ROOT, 'sandbox')
|
|
45
46
|
|
|
@@ -71,9 +72,6 @@ function reportAbsentScenarioTree(): boolean {
|
|
|
71
72
|
return true
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/** Holds fixture content for scenarios rather than scenarios of its own. */
|
|
75
|
-
const FIXTURES_DIR = 'fixtures'
|
|
76
|
-
|
|
77
75
|
/**
|
|
78
76
|
* The provision verb takes positional words rather than options, since `reset`
|
|
79
77
|
* and `clean` share its slot with a target, so its usage is written out here
|
|
@@ -125,17 +123,16 @@ interface CoverageOptions {
|
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
function getCategories(): string[] {
|
|
128
|
-
return
|
|
129
|
-
.filter((d) => d.isDirectory() && d.name !== FIXTURES_DIR)
|
|
130
|
-
.map((d) => d.name)
|
|
131
|
-
.sort()
|
|
126
|
+
return listCategories(SANDBOX_DIR)
|
|
132
127
|
}
|
|
133
128
|
|
|
129
|
+
/** Lists a stem in both forms too, so picking it reaches the refusal that names it. */
|
|
134
130
|
function getCommands(category: string): string[] {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const { scenarios, ambiguous } = listScenarioFiles(
|
|
132
|
+
join(SANDBOX_DIR, category),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return [...scenarios.map((s) => s.command), ...ambiguous].sort()
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
async function interactivePicker(): Promise<string> {
|
package/src/commands/secrets.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { type ScanRefusal, scanShippedTree } from '@/secrets/scan'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
intro,
|
|
6
|
+
logError,
|
|
7
|
+
logInfo,
|
|
8
|
+
logStep,
|
|
9
|
+
logWarn,
|
|
10
|
+
outro,
|
|
11
|
+
plural,
|
|
12
|
+
} from '@/cli/ui'
|
|
5
13
|
|
|
6
14
|
interface ScanCommandOptions {
|
|
7
15
|
readonly json?: boolean
|
package/src/commands/sessions.ts
CHANGED
package/src/commands/slides.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { INSTALL_BROWSER } from '@/browser/engine'
|
|
4
|
-
import { creationRel, SCRATCH } from '@/record
|
|
4
|
+
import { creationRel, SCRATCH } from '@/roots/record'
|
|
5
5
|
import { listDecks, resolveDeck, SLIDES_FOLDER } from '@/slides/decks'
|
|
6
6
|
import { openDeck } from '@/slides/open'
|
|
7
|
-
import { intro, outro, palette } from '@/ui'
|
|
7
|
+
import { intro, outro, palette } from '@/cli/ui'
|
|
8
8
|
|
|
9
9
|
const SLIDES_AT = creationRel(process.cwd(), SLIDES_FOLDER)
|
|
10
10
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
-
import { cliPath, cliRun } from '@/cli
|
|
5
|
-
import { PROJECT_ROOT } from '@/project
|
|
4
|
+
import { cliPath, cliRun } from '@/cli/run'
|
|
5
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
6
6
|
import {
|
|
7
7
|
buildCheckReport,
|
|
8
8
|
type CheckReport,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
logWarn,
|
|
31
31
|
outro,
|
|
32
32
|
palette,
|
|
33
|
-
} from '@/ui'
|
|
33
|
+
} from '@/cli/ui'
|
|
34
34
|
import { describeSkew } from '@/version/skew'
|
|
35
35
|
|
|
36
36
|
const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
|
package/src/commands/targets.ts
CHANGED
package/src/commands/tasks.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import {
|
|
43
43
|
type Finding,
|
|
44
44
|
type FolderClaim,
|
|
45
|
+
type SharedFiles,
|
|
45
46
|
type WideToken,
|
|
46
47
|
type Unplaced,
|
|
47
48
|
type Untested,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
logWarn,
|
|
59
60
|
outro,
|
|
60
61
|
pipeOutput,
|
|
61
|
-
} from '@/ui'
|
|
62
|
+
} from '@/cli/ui'
|
|
62
63
|
import { mainWorktreeRoot } from '@/git/worktree'
|
|
63
64
|
|
|
64
65
|
/** Returned when the board carries a finding, which is the gating result. */
|
|
@@ -239,7 +240,8 @@ export function register(program: Command): void {
|
|
|
239
240
|
' no Needs a plan row has a live plan, and every Up next row has one',
|
|
240
241
|
' every task file carries a board row or a backlog line, never both',
|
|
241
242
|
' no task carries more than one row',
|
|
242
|
-
'
|
|
243
|
+
' every Run now row names the files it touches',
|
|
244
|
+
' a file two Run now rows share is listed, never failed',
|
|
243
245
|
'',
|
|
244
246
|
'Exit codes:',
|
|
245
247
|
' 0 every check passed',
|
|
@@ -1383,7 +1385,7 @@ function reportValidation(
|
|
|
1383
1385
|
logStep(outcome.findings.length === 0 ? 'Clean' : 'Findings')
|
|
1384
1386
|
if (outcome.findings.length === 0) {
|
|
1385
1387
|
logInfo(
|
|
1386
|
-
'every row resolves, every task sits on one surface, and each
|
|
1388
|
+
'every row resolves, every task sits on one surface, and each run now row names its files',
|
|
1387
1389
|
)
|
|
1388
1390
|
} else {
|
|
1389
1391
|
for (const finding of outcome.findings) logWarn(describe(finding))
|
|
@@ -1412,6 +1414,16 @@ function reportValidation(
|
|
|
1412
1414
|
for (const claim of outcome.claims) logWarn(describeClaim(claim))
|
|
1413
1415
|
}
|
|
1414
1416
|
|
|
1417
|
+
// A shared file holds nothing under the dispatch gate, so the pairs report
|
|
1418
|
+
// with the warn glyph and move no exit code. They stay listed for merge
|
|
1419
|
+
// order and for the holds a Touches cell cannot state.
|
|
1420
|
+
logStep('Shared files')
|
|
1421
|
+
if (outcome.shared.length === 0) {
|
|
1422
|
+
logInfo('no two run now rows name the same file')
|
|
1423
|
+
} else {
|
|
1424
|
+
for (const pair of outcome.shared) logWarn(describeShared(pair))
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1415
1427
|
// The width limit is a heuristic over one preview pane, so a wide token
|
|
1416
1428
|
// reports and moves no exit code, and a measure failing on a cell that
|
|
1417
1429
|
// fits would teach a reader to skip it.
|
|
@@ -1447,6 +1459,7 @@ function reportValidation(
|
|
|
1447
1459
|
findings: outcome.findings,
|
|
1448
1460
|
untested: outcome.untested,
|
|
1449
1461
|
claims: outcome.claims,
|
|
1462
|
+
shared: outcome.shared,
|
|
1450
1463
|
wide: outcome.wide,
|
|
1451
1464
|
unplaced: outcome.unplaced,
|
|
1452
1465
|
})}\n`,
|
|
@@ -1469,6 +1482,10 @@ function describeClaim(claim: FolderClaim): string {
|
|
|
1469
1482
|
return `${claim.group}: ${claim.subject} ${claim.message}`
|
|
1470
1483
|
}
|
|
1471
1484
|
|
|
1485
|
+
function describeShared(pair: SharedFiles): string {
|
|
1486
|
+
return `${pair.group}: ${pair.subject} ${pair.message}`
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1472
1489
|
function describeWide(token: WideToken): string {
|
|
1473
1490
|
return `${token.group}: ${token.subject} ${token.message}`
|
|
1474
1491
|
}
|