@erclx/canon 4.69.0 → 4.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/canon-cli/SKILL.md +4 -0
- package/claude/skills/deploy-cloudflare/REQUIREMENT.md +37 -0
- package/claude/skills/deploy-cloudflare/SKILL.md +61 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +1 -1
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/{identity → draft-identity}/REQUIREMENT.md +2 -2
- package/claude/skills/{identity → draft-identity}/SKILL.md +2 -2
- package/claude/skills/{canon-screencast → draft-screencast}/REQUIREMENT.md +4 -4
- package/claude/skills/{canon-screencast → draft-screencast}/SKILL.md +4 -4
- package/claude/skills/{canon-slides-draft → draft-slides}/REQUIREMENT.md +3 -3
- package/claude/skills/{canon-slides-draft → draft-slides}/SKILL.md +2 -2
- package/claude/skills/{canon-frames-read → read-frames}/REQUIREMENT.md +2 -2
- package/claude/skills/{canon-frames-read → read-frames}/SKILL.md +3 -3
- package/claude/skills/{canon-record → record-screencast}/REQUIREMENT.md +5 -5
- package/claude/skills/{canon-record → record-screencast}/SKILL.md +4 -4
- package/claude/skills/review-pr/SKILL.md +55 -7
- package/claude/skills/role-orchestrator/SKILL.md +2 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +7 -3
- package/claude/skills/role-orchestrator/scripts/poll.sh +79 -34
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/ux-audit/SKILL.md +3 -0
- package/docs/agents/commands.md +2 -0
- package/docs/agents/demo.md +3 -3
- package/docs/agents/index.md +1 -1
- package/docs/agents/pr-reads.md +47 -12
- package/docs/agents/tasks.md +46 -3
- package/docs/workflow/ai-workflow.md +19 -19
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/package.json +3 -2
- package/scripts/core/regen-web-previews.ts +94 -0
- package/src/claude/cases/misc.ts +5 -1
- package/src/claude/cases/workflow.ts +4 -4
- package/src/commands/demo.ts +1 -1
- package/src/commands/pr.ts +130 -1
- package/src/commands/tasks.ts +178 -1
- package/src/commands/teach.ts +2 -0
- package/src/demo/beats.ts +1 -1
- package/src/gate/measures.ts +66 -0
- package/src/gate/stages.ts +11 -0
- package/src/migrate/skill-names.ts +25 -1
- package/src/pr/review-scope.ts +177 -0
- package/src/tasks/archive.ts +206 -3
- package/src/tasks/label.ts +14 -6
- package/src/tasks/validate.ts +22 -0
- package/src/teach/workspace.ts +60 -23
- package/standards/plan.md +1 -1
- package/standards/tasks.md +15 -1
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +103 -0
- package/tooling/cloudflare/manifest.toml +5 -0
- package/tooling/cloudflare/reference.md +24 -0
|
@@ -45,12 +45,10 @@ BASE_BRANCH="${BASE_REF#origin/}"
|
|
|
45
45
|
# `## Review response` because neither answers a comment already on the thread,
|
|
46
46
|
# which is why both were kept outside the `## Review` family rather than folded
|
|
47
47
|
# into it.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
] | last | .commit.oid // empty
|
|
53
|
-
'
|
|
48
|
+
#
|
|
49
|
+
# The review family reaches this file through `canon pr review-state` rather
|
|
50
|
+
# than through a filter of its own, so the two headings are pinned here only in
|
|
51
|
+
# the fallback that answers for a target whose CLI predates that verb.
|
|
54
52
|
# The count alone answers whether a reply is new to this script, which is not
|
|
55
53
|
# the same question as whether it is newer than the pass it answers. The stamp
|
|
56
54
|
# of the newest reply comes out of the same selection so the recency test costs
|
|
@@ -90,38 +88,72 @@ JQ_UNMATCHED_STATE='
|
|
|
90
88
|
+ (($unclassified | last) // "none")
|
|
91
89
|
'
|
|
92
90
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
91
|
+
# Four fields describe the last review pass: the commit it covered, whether it
|
|
92
|
+
# left anything owed, how long it has sat, and the instant it read. All four come
|
|
93
|
+
# out of `canon pr review-state`, which is the one place the read-time marker
|
|
94
|
+
# `review-pr` writes into its own body is parsed. Reading `commit.oid` and
|
|
95
|
+
# `submittedAt` off the thread here instead is what this replaces: GitHub stamps
|
|
96
|
+
# both when a review is submitted, so a push landing between a pass's read and
|
|
97
|
+
# its post moves them onto a commit that pass never opened, and `SEEN` below then
|
|
98
|
+
# reports it covered. That is the failure that loses work silently, measured on
|
|
99
|
+
# a pull request in this toolkit on 2026-09-07.
|
|
98
100
|
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# script runs wherever the plugin is installed. A review carrying no stamp reads
|
|
105
|
-
# as age zero and classifies nothing, which is the same answer the carry-forward
|
|
106
|
-
# path gives a pull request this run could not read.
|
|
101
|
+
# `review-pr` posts `## Review` exactly when a dispatch is owed, per the
|
|
102
|
+
# threshold that skill states, so the heading is what separates a thread waiting
|
|
103
|
+
# on a worker from one nothing is owed on. A project editing the two comment
|
|
104
|
+
# filters above for its own headings has nothing to edit here, since the verb
|
|
105
|
+
# owns the review family now.
|
|
107
106
|
#
|
|
108
|
-
# The
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
|
|
107
|
+
# The age is derived from `submittedAt` alone and the pass instant from
|
|
108
|
+
# `readAt // submittedAt`, which is deliberate rather than an oversight. The age
|
|
109
|
+
# measures how long a posted comment has waited on a human, a question about the
|
|
110
|
+
# submission, and the pass instant bounds what that pass had read, a question
|
|
111
|
+
# about the read. jq computes the elapsed seconds itself, since `date -d` is
|
|
112
|
+
# GNU-only and this script runs wherever the plugin is installed. A review
|
|
113
|
+
# carrying no stamp reads as age zero and classifies nothing, which is the same
|
|
114
|
+
# answer the carry-forward path gives a pull request this run could not read, and
|
|
115
|
+
# reads as pass instant zero, where it sends a reply to be reported rather than
|
|
116
|
+
# suppressed. The two readers fail in opposite directions on the same absent
|
|
117
|
+
# field on purpose.
|
|
118
|
+
#
|
|
119
|
+
# `source` is the field this branches on rather than the exit status, since an
|
|
120
|
+
# operator shell profile can wrap canon in a function that flattens every
|
|
121
|
+
# refusal to zero. A refusal record carries `reason` and no `source`, and it
|
|
122
|
+
# yields nothing here, which is what sends the read to the fallback below. A
|
|
123
|
+
# record read as a bare answer instead would report every refusal as a pull
|
|
124
|
+
# request nobody has reviewed.
|
|
125
|
+
JQ_SCOPE_FROM_VERB='
|
|
126
|
+
if (.source // "") == "" then empty else
|
|
127
|
+
(.commit // "none") as $prior
|
|
128
|
+
| (.state // "none") as $heading
|
|
129
|
+
| (if .submittedAt == null then 0
|
|
130
|
+
else (.submittedAt | fromdateiso8601 | floor) end) as $at
|
|
131
|
+
| (if .readAt == null then $at
|
|
132
|
+
else (.readAt | fromdateiso8601 | floor) end) as $read
|
|
133
|
+
| $prior + " " + $heading
|
|
134
|
+
+ " " + (if $at == 0 then "0" else (((now | floor) - $at) | tostring) end)
|
|
135
|
+
+ " " + ($read | tostring)
|
|
136
|
+
end
|
|
137
|
+
'
|
|
138
|
+
# The fallback for a target whose CLI predates the verb, mirroring the `canon pr
|
|
139
|
+
# head` fallback below. It reads the submission stamps and therefore carries the
|
|
140
|
+
# defect the verb closes, which is the behavior this poll already had. It does
|
|
141
|
+
# not parse the marker: a second reader of that format here is the drift the
|
|
142
|
+
# verb exists to prevent, and one that lags the format silently reports a
|
|
143
|
+
# reviewed commit as unreviewed.
|
|
144
|
+
JQ_SCOPE_FALLBACK='
|
|
115
145
|
[ .reviews[]
|
|
116
146
|
| select((.body // "") | split("\n")[0] | rtrimstr("\r")
|
|
117
147
|
| . == "## Review" or . == "## Review closed")
|
|
118
148
|
] | last
|
|
119
|
-
| if . == null then "none 0 0"
|
|
149
|
+
| if . == null then "none none 0 0"
|
|
120
150
|
else (if .submittedAt == null then 0
|
|
121
151
|
else (.submittedAt | fromdateiso8601 | floor)
|
|
122
152
|
end) as $at
|
|
123
|
-
| ((.
|
|
124
|
-
|
|
153
|
+
| ((.commit.oid // "none")
|
|
154
|
+
+ " "
|
|
155
|
+
+ (.body | split("\n")[0] | rtrimstr("\r")
|
|
156
|
+
| if . == "## Review" then "open" else "closed" end)
|
|
125
157
|
+ " "
|
|
126
158
|
+ (if $at == 0 then "0" else (((now | floor) - $at) | tostring) end)
|
|
127
159
|
+ " "
|
|
@@ -164,7 +196,7 @@ carry_forward() {
|
|
|
164
196
|
}
|
|
165
197
|
|
|
166
198
|
snapshot() {
|
|
167
|
-
local numbers n payload head prior resp merges review_state
|
|
199
|
+
local numbers n payload head prior resp merges scope review_scope review_state
|
|
168
200
|
git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
|
|
169
201
|
|
|
170
202
|
# A failed list reaches the caller as no open pull requests, and that reports
|
|
@@ -211,16 +243,29 @@ snapshot() {
|
|
|
211
243
|
continue
|
|
212
244
|
fi
|
|
213
245
|
|
|
214
|
-
prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
|
|
215
246
|
# Split here rather than carried whole, because the count keeps the fourth
|
|
216
247
|
# column every baseline written so far already reads, and the stamp goes to
|
|
217
248
|
# the end of the line beside the pass stamp it is compared against.
|
|
218
249
|
reply_state=$(jq -r "$JQ_REPLY_STATE" <<<"$payload")
|
|
219
250
|
resp=${reply_state%% *}
|
|
220
251
|
reply_at=${reply_state##* }
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
|
|
252
|
+
|
|
253
|
+
# The marker `review-pr` writes is the authority for what a pass covered,
|
|
254
|
+
# and the verb is the only reader of it. The trailing assignment is
|
|
255
|
+
# load-bearing under `set -e` and `set -o pipefail`: every refusal exits 1,
|
|
256
|
+
# and a refusal here is ordinary rather than exceptional, so an unguarded
|
|
257
|
+
# pipeline would end the whole poll on the first pull request it could not
|
|
258
|
+
# answer for.
|
|
259
|
+
scope=$(canon pr review-state "$n" --json 2>/dev/null) || scope=""
|
|
260
|
+
review_scope=$(jq -r "$JQ_SCOPE_FROM_VERB" <<<"$scope" 2>/dev/null) || review_scope=""
|
|
261
|
+
if [ -z "$review_scope" ]; then
|
|
262
|
+
review_scope=$(jq -r "$JQ_SCOPE_FALLBACK" <<<"$payload")
|
|
263
|
+
fi
|
|
264
|
+
# Four space-separated fields. The commit leads, so the line below carries
|
|
265
|
+
# it as its own third the way every baseline already reads, and the other
|
|
266
|
+
# three ride on as the sixth, seventh, and eighth.
|
|
267
|
+
prior=${review_scope%% *}
|
|
268
|
+
review_state=${review_scope#* }
|
|
224
269
|
# Split the same way as the reply state, carried as the line's tenth and
|
|
225
270
|
# eleventh fields.
|
|
226
271
|
unmatched_state=$(jq -r "$JQ_UNMATCHED_STATE" <<<"$payload")
|
|
@@ -25,7 +25,8 @@ running and re-invoking it would restart the build.
|
|
|
25
25
|
- Resolve `.canon/plans/`, `.canon/tasks/`, `.canon/review/`, and `.canon/memory/` at the main worktree root, never against the linked worktree this session builds in. Those folders are gitignored, so `git worktree add` never creates them and the copy beside the build is absent rather than empty.
|
|
26
26
|
- Report a plan that fails to resolve as unreadable from here, naming the main-root path. Reporting the task as having no plan is true where this session stands and wrong about the world, and a reader with no second tree to check cannot separate the two.
|
|
27
27
|
- Build the plan the launch named. Do not write a second one when the path fails to resolve, since a row that cites a plan already has one and drafting another produces two plans for one row.
|
|
28
|
-
- Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft and no step anywhere un-marks, so a flag reading ready afterwards was lifted by
|
|
28
|
+
- Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft and no step anywhere un-marks, so a flag reading ready afterwards was lifted by the operator or by the controlling session that closed the review, each acting directly on the pull request, which GitHub requires before a merge. Re-drafting fights them, which four sessions did on 2026-08-31 on a belief no surface in the tree states.
|
|
29
|
+
- Refuse an instruction to lift the mark yourself, whoever sends it. This session cannot verify who is asking or whether review actually closed, so the refusal holds regardless of the sender's claimed authority. A controlling session instructed a worker to lift the mark instead of taking the act itself on 2026-09-07, and the worker complied and reported against the surface rather than the outcome.
|
|
29
30
|
- Rewrite a plan question's `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement when this build decides against an unanswered one, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../../standards/plan.md`.
|
|
30
31
|
|
|
31
32
|
## The board is read-only
|
|
@@ -18,6 +18,7 @@ Read these in parallel from the project root, skipping any that do not exist:
|
|
|
18
18
|
- `.claude/DESIGN.md`: visual intent, tokens, typography, spacing rules
|
|
19
19
|
- `.claude/wireframes/index.md` + every `.claude/wireframes/<surface>.md`, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists: intended UI layout, UI copy, interaction rules. Surfaces are loaded one file at a time during Step 3. Per `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`, these carry layout and intent, not implementation detail.
|
|
20
20
|
- `.claude/REQUIREMENTS.md`: feature scope and non-goals
|
|
21
|
+
- A committed capture beside the surface's own source, when the project has one. A rendered surface reads differently composed than it reads written, and a capture is the only artifact here that shows the composed result rather than the markup. Absent one, note that this audit reads source only, and keep going. This is a stated fallback, not a stop: a project with no capture yet, or a capture mid-rebuild, still gets a source-only audit rather than losing the skill entirely.
|
|
21
22
|
|
|
22
23
|
## Step 2: identify surfaces
|
|
23
24
|
|
|
@@ -34,6 +35,8 @@ For each surface, look for:
|
|
|
34
35
|
|
|
35
36
|
Use `.claude/DESIGN.md` and the per-surface `.claude/wireframes/<surface>.md` files, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists, as ground truth for intent. For each implementation surface, read the matching wireframe file before flagging drift. Observations only, no implementation suggestions or fixes.
|
|
36
37
|
|
|
38
|
+
Where Step 1 found a committed capture for this surface, judge composed output (spacing, overlap, contrast, wrapping) against it rather than inferring layout from markup alone, which reports what the surface renders rather than what its source implies. Where none exists, judge from source alone and say so in that surface's findings, since a source-only read cannot see what only a render would show.
|
|
39
|
+
|
|
37
40
|
## Step 4: report and persist
|
|
38
41
|
|
|
39
42
|
### Report format
|
package/docs/agents/commands.md
CHANGED
|
@@ -32,6 +32,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
32
32
|
| `canon feedback` | Write toolkit feedback from stdin to `.canon/review/feedback/`, or open a GitHub issue with `--github`, refusing either when a required field is absent |
|
|
33
33
|
| `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
34
34
|
| `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
|
|
35
|
+
| `canon tasks decline` | Move a decided-against task and its plan into `.canon/tasks/declined/`, recording who decided and why (`--reason`, `--by`, `--json`) |
|
|
35
36
|
| `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
36
37
|
| `canon tasks plan-link` | Write or correct a task's `Plan:` line to point at a plan, by stem and plan path or slug (`--json`) |
|
|
37
38
|
| `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
@@ -81,6 +82,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
81
82
|
| `canon pr key-changes` | Compare the files a pull request body's Key Changes names against its own diff, in both directions (`--body`, `--base`, `--json`) |
|
|
82
83
|
| `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
|
|
83
84
|
| `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
|
|
85
|
+
| `canon pr review-state` | Report the commit and instant the last review pass covered, off the pass's own marker rather than the fields GitHub stamps at submission (`--root`, `--json`) |
|
|
84
86
|
| `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
|
|
85
87
|
| `canon repo metadata apply` | Write an explicitly supplied description, homepage, or topic set to the remote through `gh repo edit` (`--description`, `--homepage`, `--topics`, `--root`, `--json`) |
|
|
86
88
|
| `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
package/docs/agents/demo.md
CHANGED
|
@@ -18,13 +18,13 @@ canon demo frames demos/inline-edit.webm --fps 2
|
|
|
18
18
|
|
|
19
19
|
## The draft and the plan are different files
|
|
20
20
|
|
|
21
|
-
`
|
|
21
|
+
`draft-screencast` drafts beats for a person. A beat carries what is on screen, one verb, what to watch for, an emphasis, and a caption, and none of that names a selector, a URL, a wait condition, or a timing. Those four are what an executor needs and what would ruin the draft, since the format is pre-seeded so the operator edits down rather than fills blanks.
|
|
22
22
|
|
|
23
23
|
So `canon demo compile` writes a second artifact rather than adding fields to a beat. It maps each beat's verb to a step, seeds the timing, marks which beat the still comes from, and leaves the target and the URL empty for a person to fill. The report names every field it could not supply.
|
|
24
24
|
|
|
25
25
|
The plan is committed, not scratch. Its timing is a starting point tuned by watching a recording, and the draft cannot reproduce a tuned value, so a recompile over an existing plan refuses and names `--force`.
|
|
26
26
|
|
|
27
|
-
`
|
|
27
|
+
`record-screencast` is the routed way to run both verbs from a draft path: it compiles only when no plan exists yet, then runs, stopping to report any field still unresolved rather than guessing one.
|
|
28
28
|
|
|
29
29
|
| Option | Behavior |
|
|
30
30
|
| --------------- | ----------------------------------------------------------------- |
|
|
@@ -64,7 +64,7 @@ A step waits on its `waitFor` selector becoming visible and then holds for its o
|
|
|
64
64
|
| `--fps <n>` | Frames extracted per second of video, default `1` |
|
|
65
65
|
| `--json` | Add a record on stdout carrying every frame path written |
|
|
66
66
|
|
|
67
|
-
`
|
|
67
|
+
`read-frames` is the routed way to call this verb and read the frames back: it runs the verb, reads each returned frame with the Read tool, and reports one plain description per frame. It never renders a verdict, since a frame read is evidence a person weighs rather than a pass or fail this toolkit states on their behalf.
|
|
68
68
|
|
|
69
69
|
## The pointer is painted inside the page
|
|
70
70
|
|
package/docs/agents/index.md
CHANGED
|
@@ -28,7 +28,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
28
28
|
- [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
|
|
29
29
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
30
30
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
31
|
-
- [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading
|
|
31
|
+
- [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
|
|
32
32
|
- [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
33
33
|
- [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
|
|
34
34
|
- [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
|
package/docs/agents/pr-reads.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Head-sensitive pull request reads
|
|
3
|
-
description: Resolving a branch tip from the remote rather than from the pull request object, reading
|
|
3
|
+
description: Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Head-sensitive pull request reads
|
|
7
7
|
|
|
8
|
-
`canon pr head` and `canon pr
|
|
8
|
+
`canon pr head`, `canon pr checks`, and `canon pr review-state` each answer about a commit, and each exists because the field GitHub offers answers a different question. The first two resolve the branch tip from the remote with `git ls-remote` rather than taking the pull request object's word for which commit the branch is on. The third reads what a review pass covered off a marker the pass wrote, rather than off the head GitHub stamped when the review was submitted.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
canon pr head
|
|
12
12
|
canon pr head 1341 --json
|
|
13
13
|
canon pr checks
|
|
14
14
|
canon pr checks 1341 --json
|
|
15
|
+
canon pr review-state
|
|
16
|
+
canon pr review-state 1341 --json
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
## The pull request object is not the authority for a head
|
|
@@ -30,8 +32,40 @@ The argument is stated here once. The shipped skill bodies call the verb rather
|
|
|
30
32
|
|
|
31
33
|
`canon pr checks` reads `repos/{owner}/{repo}/commits/<tip>/check-runs` and collapses it to `passing`, `failing`, or `pending`. A failure outranks a run still going, since a run in flight cannot clear a job that already failed.
|
|
32
34
|
|
|
35
|
+
`canon pr review-state` reports `heading` and `state` for the newest pass in the review family, `commit` and `readAt` for what it covered, `submittedAt` for when GitHub recorded it, and `source` for which of those readings answered. A thread carrying no pass reports `state` and `source` both as `none` with nothing else on the record.
|
|
36
|
+
|
|
33
37
|
`gh pr checks` cannot be made to answer this question at all, which is the argument for the move rather than a preference between two working commands. Its `--json` field set is `bucket, completedAt, description, event, link, name, startedAt, state, workflow`, with no sha among them, so a caller cannot even learn which commit its answer describes.
|
|
34
38
|
|
|
39
|
+
## A review's stamps describe the submission, not the read
|
|
40
|
+
|
|
41
|
+
`commit.oid` on a review names whatever the pull request head was at the instant the review was submitted. `submittedAt` names that instant. Neither names the commit the reviewing session actually opened, and the two are only the same when nothing was pushed while the comment was being written.
|
|
42
|
+
|
|
43
|
+
A push landing in that window moves both onto a commit nobody reviewed. The next pass then scopes its delta past that commit and reports it covered, which is the failure direction that loses work: the commit is skipped rather than re-read. It fired on a pull request in this toolkit on 2026-09-07, where a pass read one commit, the branch pushed another during the compose, and the poll reported the second as already seen. What it skipped was a genuine fix, and a reader comparing the sha in the report against the sha in the pass caught it.
|
|
44
|
+
|
|
45
|
+
`review-pr` therefore ends every body it posts with a marker naming what it read:
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
<!-- review-pr: commit=<sha> read-at=<iso8601> -->
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both values are taken at the top of the pass rather than at the post, so the compose window falls outside what the marker claims. An HTML comment renders as nothing on GitHub, so a reader of the thread never meets it.
|
|
52
|
+
|
|
53
|
+
The last non-empty line is where it has to sit, and the reader looks nowhere else. Searching a body for the last match instead reads a marker the body was displaying rather than claiming, since a fenced block showing the format on its own line trims to exactly the pattern, and a pass carrying no marker of its own would then hand the next reader a commit taken from an illustration. That is the same defect through a second door, so position rather than shape is what separates a claim from a quotation.
|
|
54
|
+
|
|
55
|
+
`canon pr review-state` is the only parser of that format. Two parsers was the alternative, one in the shipped skill body and one in the orchestrator poll's jq, and those two ship on different cadences, so a format change to either would leave the other reading a reviewed commit as unreviewed.
|
|
56
|
+
|
|
57
|
+
`source` on the record says which reading answered, and a caller checks it before trusting the rest:
|
|
58
|
+
|
|
59
|
+
| Source | What it means |
|
|
60
|
+
| ---------- | ------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `marker` | The pass wrote its own read-time record, which is the authority |
|
|
62
|
+
| `fallback` | A pass posted before this shipped, read off the submission stamps and carrying their defect |
|
|
63
|
+
| `none` | The thread carries no pass, so the next one is a first pass |
|
|
64
|
+
|
|
65
|
+
The record carries `submittedAt` beside `readAt` rather than in place of it, because each is right about a different question. What a pass had read is a fact about the read, and how long a posted comment has waited on a human is a fact about the submission. The orchestrator poll reads the first for its coverage test and the second for its age test.
|
|
66
|
+
|
|
67
|
+
A `PUT` rewrite of a standing close-out inherits the fix without a mechanism of its own. That request replaces the body and the marker rides in it, so a rewritten comment names the commit the rewriting pass read. `submittedAt` stays pinned to the original submission, which is the reading the age test wants anyway.
|
|
68
|
+
|
|
35
69
|
## An empty run list is not a pass
|
|
36
70
|
|
|
37
71
|
Keying the query on a sha is necessary and not sufficient. The endpoint answered with `total_count` 2 and an empty row list during a measured window on 2026-09-02, so a reader that finds no run for the tip and reports `passing` reproduces the false green behind a better query.
|
|
@@ -50,15 +84,16 @@ Branch on the record rather than on the exit. Both verbs exit 0 whenever they re
|
|
|
50
84
|
|
|
51
85
|
Each refusal names a different repair:
|
|
52
86
|
|
|
53
|
-
| Reason
|
|
54
|
-
|
|
|
55
|
-
| `gh-missing`
|
|
56
|
-
| `gh-failed`
|
|
57
|
-
| `no-branch`
|
|
58
|
-
| `unresolvable-ref`
|
|
59
|
-
| `no-remote-branch`
|
|
60
|
-
| `no-object-head`
|
|
61
|
-
| `runs-unreadable`
|
|
87
|
+
| Reason | What it means |
|
|
88
|
+
| -------------------- | ------------------------------------------------------------------------------------------ |
|
|
89
|
+
| `gh-missing` | `gh` is not on the path, so no pull request resolved |
|
|
90
|
+
| `gh-failed` | `gh` could not answer for this branch, so name a number instead |
|
|
91
|
+
| `no-branch` | The pull request carries no head branch name, so no ref could be read |
|
|
92
|
+
| `unresolvable-ref` | The remote read failed, which is not the same answer as an absent branch |
|
|
93
|
+
| `no-remote-branch` | The remote carries no branch by that name, so there is no tip to compare against |
|
|
94
|
+
| `no-object-head` | The pull request object reported no head, so `head` has nothing to compare the tip against |
|
|
95
|
+
| `runs-unreadable` | The check runs for the tip could not be read, which is unread rather than none |
|
|
96
|
+
| `reviews-unreadable` | The reviews on the pull request could not be parsed, which is unread rather than none |
|
|
62
97
|
|
|
63
98
|
A failed remote read and a branch the remote does not carry stay apart rather than collapsing into one reason. Reading the first as the second would report a network refusal as a deleted branch, and the repairs have nothing in common.
|
|
64
99
|
|
|
@@ -68,6 +103,6 @@ One remote round trip per call, sampled here at 0.41s, 0.55s, and 0.67s on 2026-
|
|
|
68
103
|
|
|
69
104
|
One head-sensitive read is left on the object deliberately. `canon targets pulls` reads `statusCheckRollup` for every open pull request across every target, where resolving a tip per row would cost one remote read per pull request across a dozen projects, against a surface that reports a listing rather than gating a push.
|
|
70
105
|
|
|
71
|
-
The orchestrator poll spends that same read per open pull request and repeats it on a three-minute timer, which makes it the heaviest caller here rather than an exception to the paragraph above. Six open pull requests is around 120 remote reads an hour. What separates the two cases is what each reading decides rather than what it costs. The poll's head fires the review trigger, so a stale one sends a pass at a commit nobody read or withholds one that is owed. The listing decides nothing, so the same spend buys a fresher column in a report and no correctness at all.
|
|
106
|
+
The orchestrator poll spends that same read per open pull request and repeats it on a three-minute timer, which makes it the heaviest caller here rather than an exception to the paragraph above. Six open pull requests is around 120 remote reads an hour. It now spends a third read per pull request per tick besides, since `canon pr review-state` makes its own `gh pr view` call beside the payload the poll already fetched. That one is an API round trip rather than a remote ref read, and it buys a coverage test that answers about what a session read. What separates the two cases is what each reading decides rather than what it costs. The poll's head fires the review trigger, so a stale one sends a pass at a commit nobody read or withholds one that is owed. The listing decides nothing, so the same spend buys a fresher column in a report and no correctness at all.
|
|
72
107
|
|
|
73
108
|
`canon pr key-changes` also stays on the object, for a different reason. It reads the body, the file list, and `headRefOid` in one call on purpose, so the three describe one commit. That is a consistency requirement rather than a freshness one, and keying its head elsewhere would break it.
|
package/docs/agents/tasks.md
CHANGED
|
@@ -72,6 +72,36 @@ Skills branch on the reason rather than on the exit code:
|
|
|
72
72
|
canon tasks archive --pull-request 673 --json | jq -r 'if .ok then .task else .reason end'
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Decline
|
|
76
|
+
|
|
77
|
+
`canon tasks decline <task>` moves a task decided against from `.canon/tasks/` into `.canon/tasks/declined/`, clears whichever of `priority.md` or `backlog.md` holds its row, and regenerates the board index. Unlike `canon tasks archive`, it carries no outcome-state gate: a task can be decided against at any outcome state.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
canon tasks decline v28.1-trigger-escalation --reason "superseded by v30.2" # canon-allow-reference: illustrates the stem-selection form, not a citation of a real task
|
|
81
|
+
canon tasks decline v28.1-trigger-escalation --reason "no longer needed" --by Alex --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Option | Behavior |
|
|
85
|
+
| ----------------- | ------------------------------------------------- |
|
|
86
|
+
| `--reason <text>` | Why the task was decided against, required |
|
|
87
|
+
| `--by <name>` | Who decided, defaulting to `git config user.name` |
|
|
88
|
+
| `--json` | Emit a machine-readable record on stdout |
|
|
89
|
+
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
90
|
+
|
|
91
|
+
Exit codes: `0` declined, `1` refused. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, or `bad-input`. `bad-input` covers a missing `--reason` and a `--by` that resolves to nobody, git config included.
|
|
92
|
+
|
|
93
|
+
`DECLINE_REFUSALS` is kept apart from archive's own refusal set on purpose. Archive and decline answer different questions, shipped versus decided-against, and a shared gate would let one archive a task that cannot yet ship or decline one that already has.
|
|
94
|
+
|
|
95
|
+
The decision is written onto the task as a `Declined: <reason>, <who> on <YYYY-MM-DD>` line, anchored the same way `Pull request:` is, after the last origin line the task carries.
|
|
96
|
+
|
|
97
|
+
The task carries its plan with it the same way archive does, when the declining task is the last live citation. A declined task's plan lands in `.canon/plans/archive/`, indistinguishable from a shipped one by folder alone. The task file under `.canon/tasks/declined/` is what records which it was.
|
|
98
|
+
|
|
99
|
+
Skills branch on the reason rather than on the exit code, the same rule `canon tasks archive` states:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
canon tasks decline v28.1-trigger-escalation --reason "superseded by v30.2" --json | jq -r 'if .ok then .task else .reason end' # canon-allow-reference: illustrates the stem-selection form, not a citation of a real task
|
|
103
|
+
```
|
|
104
|
+
|
|
75
105
|
## Plan citations
|
|
76
106
|
|
|
77
107
|
`canon tasks plan-citations <stem>` answers where a task's plan sits and which other live tasks hold it. It reports and never writes.
|
|
@@ -244,7 +274,7 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
|
|
|
244
274
|
| Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
|
|
245
275
|
| Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
|
|
246
276
|
| Collisions | Two `## Run now` rows whose Touches columns name a path in common |
|
|
247
|
-
| Blockers | A parked row whose blocker has stopped holding, or whose cited task
|
|
277
|
+
| Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
|
|
248
278
|
|
|
249
279
|
Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
|
|
250
280
|
|
|
@@ -264,7 +294,20 @@ The collision check is the one a person cannot run by eye. Paths come from the b
|
|
|
264
294
|
|
|
265
295
|
Where a directory holds the other row's file, the finding names the row that claimed it, reading `both touch src/tasks, which v2.0-second claims as a folder.` The shared strings alone leave an over-broad cell and a genuine overlap identical, which is how a correct report was once read as the verb comparing folders rather than files. <!-- canon-allow-reference: illustrates the finding's own sentence shape, not a citation of a real task -->
|
|
266
296
|
|
|
267
|
-
The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or by its work reaching the trunk, and a collision is settled by nothing under `## Run now` still holding the file the cell cites.
|
|
297
|
+
The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or by its work reaching the trunk, and a collision is settled by nothing under `## Run now` still holding the file the cell cites. A cited task resolving under `.canon/tasks/declined/` instead reports separately as `blocker-declined`, since a decided-against task is neither the shipped work `blocker-settled` reports nor the dangling pointer `blocker-unresolved` reports.
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"findings": [
|
|
302
|
+
{
|
|
303
|
+
"kind": "blocker-declined",
|
|
304
|
+
"group": "Up next",
|
|
305
|
+
"subject": "v50.6-a-standard-no-skill-reads", // canon-allow-reference: shows the subject field's real vXX.Y-slug shape, not a citation of a real task
|
|
306
|
+
"message": "waits on v9.0-superseded, which was declined." // canon-allow-reference: illustrates the finding's own sentence shape, not a citation of a real task
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
```
|
|
268
311
|
|
|
269
312
|
A closed outcome is not the fact the dependency half needs. The ship chain marks outcomes as its first step and opens the pull request several steps later, so a check reading the checkbox reports the row settled while the branch is still in review. A live task therefore settles the row only once it closed every outcome and carries a `Pull request:` line the trunk holds. One that names no pull request, and one whose number no trunk ref could answer for, land in the untested array below rather than being settled or left silent.
|
|
270
313
|
|
|
@@ -272,7 +315,7 @@ The trunk is read as the clone already holds it, `origin/main` first and local `
|
|
|
272
315
|
|
|
273
316
|
Both halves gate on a citation inside the `Waiting on` cell, never on the columns beside it. The board format gives a collision cell the file held by the running task, so a row whose cell names no file was parked by something else, and testing its Touches column instead reports a cleared collision on a row no collision ever parked while counting that row as re-tested. A cited task is a bare sibling link, the way the Task column spells one, so a pointer into another folder names a plan rather than a task and settles nothing. A cited task carrying no outcome box settles nothing either, since a file the check could not parse is not evidence of a finished one.
|
|
274
317
|
|
|
275
|
-
A citation resolving in
|
|
318
|
+
A citation resolving in none of the board, the archive, or the declined folder is `blocker-unresolved` rather than a settled row. Reading an absent file as archived or declined states a specific fact about a file nobody ever wrote, which is what a renamed task or a typo produces, and only a task that genuinely closed or was genuinely declined releases the row waiting on it.
|
|
276
319
|
|
|
277
320
|
The other three kinds rest on a person's judgment, so a row neither half reached lands in a second array rather than in the findings:
|
|
278
321
|
|
|
@@ -80,7 +80,7 @@ When features are independent, run them in parallel instead of sequentially. Use
|
|
|
80
80
|
- Invoke `canon:plan-feature` in each session. Plans land at the main worktree root as `.canon/plans/feature-<slug>.md`, one per feature, no collisions. Small features stay in chat and skip the file.
|
|
81
81
|
- Implement, verify, and review each feature independently. `review-branch` writes a per-branch report at the main worktree root (`review/branch/review-<slug>.md`), and `ui-test` writes a per-branch checklist handoff there too (`tmp/ui-checklist/<slug>.md`) that `git-pr` posts to the pull request and removes, so parallel sessions do not overwrite each other. The slug is the branch name with any leading type segment dropped, so `feat/jwt-expiration` and the plan at `feature-jwt-expiration.md` meet on one name
|
|
82
82
|
- Ship each worktree separately with `canon:git-ship`
|
|
83
|
-
- For full autonomy per worktree, invoke `canon:auto-ship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it
|
|
83
|
+
- For full autonomy per worktree, invoke `canon:auto-ship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it directly, an act reserved to the operator or to the controlling session that closed the review.
|
|
84
84
|
|
|
85
85
|
To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `canon:role-orchestrator`. It holds the cross-feature call, plans each feature itself or dispatches a cold planner under `canon:role-planner` to write the plan, refills the ready queue so a free worker never waits, and reviews each worker's PR with `canon:review-pr`, then tells the session holding that branch to run `canon:review-address` whenever the pass posted a finding at any severity, which is the same threshold `canon:review-pr` states and posts its open heading under. The human launches workers and merges. See [operating model](operating-model.md) for the full loop.
|
|
86
86
|
|
|
@@ -289,23 +289,23 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
289
289
|
|
|
290
290
|
### Generate an artifact on demand
|
|
291
291
|
|
|
292
|
-
| Skill
|
|
293
|
-
|
|
|
294
|
-
| `canon:create-rule`
|
|
295
|
-
| `canon:create-skill`
|
|
296
|
-
| `canon:create-snippet`
|
|
297
|
-
| `canon:create-standard`
|
|
298
|
-
| `canon:draft-docs`
|
|
299
|
-
| `canon:draft-context`
|
|
300
|
-
| `canon:draft-wireframes`
|
|
301
|
-
| `canon:draft-readme`
|
|
302
|
-
| `canon:bash-script`
|
|
303
|
-
| `canon:bash-cli-script`
|
|
304
|
-
| `canon:ci-workflow`
|
|
305
|
-
| `canon:
|
|
306
|
-
| `canon:
|
|
307
|
-
| `canon:
|
|
308
|
-
| `canon:identity`
|
|
292
|
+
| Skill | When to use |
|
|
293
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
294
|
+
| `canon:create-rule` | For a project-specific governance rule the toolkit does not ship |
|
|
295
|
+
| `canon:create-skill` | For a new `SKILL.md` |
|
|
296
|
+
| `canon:create-snippet` | For a reusable prompt |
|
|
297
|
+
| `canon:create-standard` | For a new authoring convention |
|
|
298
|
+
| `canon:draft-docs` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
|
|
299
|
+
| `canon:draft-context` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
|
|
300
|
+
| `canon:draft-wireframes` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
|
|
301
|
+
| `canon:draft-readme` | For a project's `README.md`, drafted against `standards/readme.md` |
|
|
302
|
+
| `canon:bash-script` | For an interactive, human-facing shell tool |
|
|
303
|
+
| `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
|
|
304
|
+
| `canon:ci-workflow` | For a GitHub Actions workflow file |
|
|
305
|
+
| `canon:draft-slides` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
306
|
+
| `canon:draft-screencast` | For a recording script with beats and defaults already seeded |
|
|
307
|
+
| `canon:record-screencast` | For compiling and running a screencast draft into a recording and a still |
|
|
308
|
+
| `canon:draft-identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
|
|
309
309
|
|
|
310
310
|
### Answer a question at any point
|
|
311
311
|
|
|
@@ -314,7 +314,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
314
314
|
| `canon:canon-cli` | Before running an unfamiliar verb, a sync, or an install, to learn which command to run, which reference doc covers it, or what it overwrites, merges, or leaves alone |
|
|
315
315
|
| `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs |
|
|
316
316
|
| `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context |
|
|
317
|
-
| `canon:
|
|
317
|
+
| `canon:read-frames` | To read a recorded demo back frame by frame and report what each one shows, with no verdict on whether the recording looks right |
|
|
318
318
|
| `canon:teach-workspace` | To learn a subject across sessions, in a workspace that holds the progress |
|
|
319
319
|
| `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density |
|
|
320
320
|
| `canon:restate-plainly` | When an answer or a document has to be read again in plain words |
|
|
@@ -40,7 +40,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
40
40
|
- `canon:ux-audit` for UX gap detection on existing surfaces
|
|
41
41
|
- `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
42
42
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
43
|
-
- `canon:identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
43
|
+
- `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
44
44
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
45
45
|
|
|
46
46
|
### When to pick
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/canon",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.71.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"web:tokens": "bun src/cli.ts design css --no-components > web/src/styles/tokens.css.new && (echo '/* Generated by `canon design css --no-components`. Regenerate with `bun run web:tokens`. Do not hand-edit. */'; cat web/src/styles/tokens.css.new) > web/src/styles/tokens.css && rm web/src/styles/tokens.css.new",
|
|
45
45
|
"web:dev": "cd web && astro dev",
|
|
46
46
|
"web:favicon": "bun scripts/core/regen-web-favicon.ts",
|
|
47
|
-
"web:
|
|
47
|
+
"web:previews": "bun scripts/core/regen-web-previews.ts",
|
|
48
|
+
"web:build": "bun run web:tokens && bun run web:favicon && bun run web:previews && cd web && astro check && astro build",
|
|
48
49
|
"web:preview": "cd web && astro preview",
|
|
49
50
|
"web:e2e": "cd web && playwright test"
|
|
50
51
|
},
|