@erclx/canon 4.51.0 ā 4.53.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/claude-address-review/SKILL.md +28 -11
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +1 -1
- package/docs/agents/commands.md +78 -78
- package/docs/agents/tasks.md +1 -1
- package/package.json +8 -2
- package/src/commands/labels.ts +6 -0
- package/src/commands/tasks.ts +5 -3
- package/src/labels/event.ts +24 -0
- package/src/records/validate.ts +80 -2
- package/src/tasks/answers.ts +19 -13
- package/tooling/astro/configs/playwright.config.ts +4 -1
- package/tooling/astro/reference.md +1 -1
- package/tooling/vite-react/configs/playwright.config.ts +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +23 -1
- package/tooling/web/configs/e2e/screenshot.ts +36 -1
- package/tooling/web/manifest.toml +2 -1
- package/tooling/web/reference.md +10 -1
|
@@ -150,12 +150,17 @@ the regen rebuilt, and close the same way. The heading stays outside the
|
|
|
150
150
|
`## Review` family so the close-out's equality test on the first line never
|
|
151
151
|
matches it.
|
|
152
152
|
|
|
153
|
-
Before posting,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
Before posting, follow `${CLAUDE_SKILL_DIR}/../../standards/publish.md`
|
|
154
|
+
and run its scan against the reply:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The hook skips `.canon/tmp/`, so this scan is the only gate on the published
|
|
161
|
+
reply. Fix any hit the standard names, then post the reply to the PR and
|
|
162
|
+
capture the posted comment's id, since Step 7 edits this exact comment rather
|
|
163
|
+
than trusting whichever one `gh` considers last:
|
|
159
164
|
|
|
160
165
|
```bash
|
|
161
166
|
comment_url=$(gh pr comment <number> --body-file .canon/tmp/address-review/reply-<number>.md)
|
|
@@ -182,13 +187,25 @@ carries none is false on a surface nothing else checks:
|
|
|
182
187
|
printf '\nā
Rebased onto origin/main, CI green. No review findings were open.\n' >> .canon/tmp/address-review/reply-<number>.md
|
|
183
188
|
```
|
|
184
189
|
|
|
185
|
-
Re-run the
|
|
186
|
-
|
|
190
|
+
Re-run the scan against the updated file, since the appended line is new
|
|
191
|
+
content the Step 6 scan never saw:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md
|
|
195
|
+
```
|
|
196
|
+
|
|
187
197
|
Then edit the exact comment Step 6 posted, read back from the id it saved,
|
|
188
|
-
rather than posting a second comment
|
|
198
|
+
rather than posting a second comment. A shell variable assigned from `cat` and
|
|
199
|
+
handed to `gh` still carries a runtime value the isolation rule cannot verify,
|
|
200
|
+
so read the id as its own plain command and write the printed value as a
|
|
201
|
+
literal in the `gh api` call rather than a substitution or a variable:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
cat .canon/tmp/address-review/reply-<number>.id
|
|
205
|
+
```
|
|
189
206
|
|
|
190
207
|
```bash
|
|
191
|
-
gh api -X PATCH "repos/{owner}/{repo}/issues/comments
|
|
208
|
+
gh api -X PATCH "repos/{owner}/{repo}/issues/comments/<id printed above>" \
|
|
192
209
|
-F body=@.canon/tmp/address-review/reply-<number>.md
|
|
193
210
|
```
|
|
194
211
|
|
|
@@ -226,7 +243,7 @@ Do not merge. Hand back to the orchestrator for re-review.
|
|
|
226
243
|
|
|
227
244
|
## Post-review findings
|
|
228
245
|
|
|
229
|
-
Not everything worth reaching the reviewing session surfaces inside the numbered flow above. A worker that settled a risk, filed a follow-up, or found something else worth reporting after Step 7 already closed the review posts it directly rather than waiting on a review pass that has nothing left to trigger it. Write the body the way Step 6 writes a reply: load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words, and run the `${CLAUDE_SKILL_DIR}/../../standards/publish.md` scan before posting.
|
|
246
|
+
Not everything worth reaching the reviewing session surfaces inside the numbered flow above. A worker that settled a risk, filed a follow-up, or found something else worth reporting after Step 7 already closed the review posts it directly rather than waiting on a review pass that has nothing left to trigger it. Write the body the way Step 6 writes a reply: load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words, and run the `${CLAUDE_SKILL_DIR}/../../standards/publish.md` scan before posting with `canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md`.
|
|
230
247
|
|
|
231
248
|
Open with `## Post-review findings` rather than `## Review response`, since nothing on the thread is being answered. `claude-pr-review` states the full heading set this belongs to and routes it the same as a response: `claude-orchestrate`'s poll picks it up and sends the reviewing session back for a pass. Close the body with `š¤ Addressed by Claude Code` on its own line, matching the reply's footer.
|
|
232
249
|
|
|
@@ -26,7 +26,7 @@ This gate runs ahead of the two collision checks because it is the cheapest read
|
|
|
26
26
|
|
|
27
27
|
It also reads the plan rather than a cell describing one, which is the input the gate below it does not have. The disjointness gate compares the sets a dispatcher wrote into the constraints and the Touches column, so a cell omitting a file clears a check the tree would fail. That happened on 2026-08-31, when two rows were cleared against each other with one constraints block leaving out the context entry both were about to write, and what caught it was a worker sending a message rather than any check.
|
|
28
28
|
|
|
29
|
-
A blank `- Answer:` is not an unanswered question. `${CLAUDE_SKILL_DIR}/../../standards/plan.md` fixes an empty slot as accepting the `- Suggested:` line above it, which is what makes a plan decision-ready in one pass. The narrow case this reads is `- Suggested: needs your call, <why>` over an empty slot, the form that same standard writes where the answer turns on preference rather than on a technical default. A gate reading every blank slot as open would refuse every plan in the folder.
|
|
29
|
+
A blank `- Answer:` is not an unanswered question. `${CLAUDE_SKILL_DIR}/../../standards/plan.md` fixes an empty slot as accepting the `- Suggested:` line above it, which is what makes a plan decision-ready in one pass. The narrow case this reads is `- Suggested: needs your call, <why>` and its two demonstrated paraphrases, `needs operator's call` and `needs the operator's call`, over an empty slot, the form that same standard writes where the answer turns on preference rather than on a technical default. A gate reading every blank slot as open would refuse every plan in the folder.
|
|
30
30
|
|
|
31
31
|
What it prevents is a halt nobody is watching for. `claude-worker` instructs a session to stop on a question written as needing the operator's call, correctly and by its own body, so a dispatch that never reads the plan lands a worker in a wait for a person who does not know it is waiting. The worker's halt is not the defect, and the dispatch that made it necessary is.
|
|
32
32
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -9,84 +9,84 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
9
9
|
|
|
10
10
|
## Project-level
|
|
11
11
|
|
|
12
|
-
| Command | Purpose
|
|
13
|
-
| ----------------------------- |
|
|
14
|
-
| `canon init [path]` | Bootstrap a project with selected toolkit domains
|
|
15
|
-
| `canon sync [path]` | Sync all installed domains in a target project
|
|
16
|
-
| `canon sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`)
|
|
17
|
-
| `canon sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads
|
|
18
|
-
| `canon sandbox reset` | Reset sandbox to baseline
|
|
19
|
-
| `canon sandbox clean` | Wipe the sandbox
|
|
20
|
-
| `canon sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict)
|
|
21
|
-
| `canon sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`)
|
|
22
|
-
| `canon indexes regen` | Regenerate `index.md` files from sibling frontmatter
|
|
23
|
-
| `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name)
|
|
24
|
-
| `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS
|
|
25
|
-
| `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts`
|
|
26
|
-
| `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone)
|
|
27
|
-
| `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css`
|
|
28
|
-
| `canon design sync` | Update a base stylesheet already installed under `.claude/design/`
|
|
29
|
-
| `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr
|
|
30
|
-
| `canon slides list` | List the available slide layouts (`--json` for the catalog)
|
|
31
|
-
| `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
|
|
32
|
-
| `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`)
|
|
33
|
-
| `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index
|
|
34
|
-
| `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`)
|
|
35
|
-
| `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`)
|
|
36
|
-
| `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`)
|
|
37
|
-
| `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`)
|
|
38
|
-
| `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`)
|
|
39
|
-
| `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`)
|
|
40
|
-
| `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds and what its records schedule (`--json`)
|
|
41
|
-
| `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`)
|
|
42
|
-
| `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`)
|
|
43
|
-
| `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`)
|
|
44
|
-
| `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`)
|
|
45
|
-
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`)
|
|
46
|
-
| `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`)
|
|
47
|
-
| `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`)
|
|
48
|
-
| `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`)
|
|
49
|
-
| `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`)
|
|
50
|
-
| `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`)
|
|
51
|
-
| `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`)
|
|
52
|
-
| `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`)
|
|
53
|
-
| `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`)
|
|
54
|
-
| `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`)
|
|
55
|
-
| `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`)
|
|
56
|
-
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`)
|
|
57
|
-
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`)
|
|
58
|
-
| `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git
|
|
59
|
-
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift
|
|
60
|
-
| `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints
|
|
61
|
-
| `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md`
|
|
62
|
-
| `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`)
|
|
63
|
-
| `canon claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`)
|
|
64
|
-
| `canon claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one
|
|
65
|
-
| `canon claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`)
|
|
66
|
-
| `canon claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`)
|
|
67
|
-
| `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`)
|
|
68
|
-
| `canon gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value and on the family stem behind a templated citation (`--json`)
|
|
69
|
-
| `canon gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`)
|
|
70
|
-
| `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`)
|
|
71
|
-
| `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`)
|
|
72
|
-
| `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`)
|
|
73
|
-
| `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`)
|
|
74
|
-
| `canon labels scan` | Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a label a code span quotes, a gitignored record path, a session link, or a title word no dictionary holds (`--event`, `--json`) |
|
|
75
|
-
| `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`)
|
|
76
|
-
| `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`)
|
|
77
|
-
| `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`)
|
|
78
|
-
| `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`)
|
|
79
|
-
| `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`)
|
|
80
|
-
| `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`)
|
|
81
|
-
| `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`)
|
|
82
|
-
| `canon audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`, `--corpus`)
|
|
83
|
-
| `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`)
|
|
84
|
-
| `canon gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`)
|
|
85
|
-
| `canon inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`)
|
|
86
|
-
| `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`)
|
|
87
|
-
| `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`)
|
|
88
|
-
| `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`)
|
|
89
|
-
| `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`)
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `canon init [path]` | Bootstrap a project with selected toolkit domains |
|
|
15
|
+
| `canon sync [path]` | Sync all installed domains in a target project |
|
|
16
|
+
| `canon sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
|
|
17
|
+
| `canon sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
|
|
18
|
+
| `canon sandbox reset` | Reset sandbox to baseline |
|
|
19
|
+
| `canon sandbox clean` | Wipe the sandbox |
|
|
20
|
+
| `canon sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
|
|
21
|
+
| `canon sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
|
|
22
|
+
| `canon indexes regen` | Regenerate `index.md` files from sibling frontmatter |
|
|
23
|
+
| `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
24
|
+
| `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
|
|
25
|
+
| `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
|
|
26
|
+
| `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
|
|
27
|
+
| `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
|
|
28
|
+
| `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
|
|
29
|
+
| `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
|
|
30
|
+
| `canon slides list` | List the available slide layouts (`--json` for the catalog) |
|
|
31
|
+
| `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 |
|
|
32
|
+
| `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
33
|
+
| `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
|
|
34
|
+
| `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
35
|
+
| `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`) |
|
|
36
|
+
| `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
37
|
+
| `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
|
|
38
|
+
| `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
39
|
+
| `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
40
|
+
| `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds and what its records schedule (`--json`) |
|
|
41
|
+
| `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
|
|
42
|
+
| `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
|
|
43
|
+
| `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
44
|
+
| `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
|
|
45
|
+
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
|
|
46
|
+
| `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
47
|
+
| `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
48
|
+
| `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
49
|
+
| `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
50
|
+
| `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
51
|
+
| `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
|
|
52
|
+
| `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
|
|
53
|
+
| `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`) |
|
|
54
|
+
| `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
|
|
55
|
+
| `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
56
|
+
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
57
|
+
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
|
|
58
|
+
| `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
59
|
+
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
|
|
60
|
+
| `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
|
|
61
|
+
| `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
62
|
+
| `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
|
|
63
|
+
| `canon claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
64
|
+
| `canon claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
65
|
+
| `canon claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`) |
|
|
66
|
+
| `canon claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`) |
|
|
67
|
+
| `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
68
|
+
| `canon gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value and on the family stem behind a templated citation (`--json`) |
|
|
69
|
+
| `canon gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`) |
|
|
70
|
+
| `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
|
|
71
|
+
| `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
72
|
+
| `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
73
|
+
| `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
74
|
+
| `canon labels scan` | Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a label a code span quotes, a gitignored record path, a session link, or a title word no dictionary holds (`--event`, `--body-file`, `--json`) |
|
|
75
|
+
| `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
|
|
76
|
+
| `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`) |
|
|
77
|
+
| `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
|
|
78
|
+
| `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
|
|
79
|
+
| `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`) |
|
|
80
|
+
| `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`) |
|
|
81
|
+
| `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
82
|
+
| `canon audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`, `--corpus`) |
|
|
83
|
+
| `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
84
|
+
| `canon gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
|
|
85
|
+
| `canon inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
|
|
86
|
+
| `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
|
|
87
|
+
| `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
|
|
88
|
+
| `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
|
|
89
|
+
| `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
|
|
90
90
|
|
|
91
91
|
`canon serve` drives no browser, which is what separates it from the four that do. All four ship now that `capture` does, so the line between them is the engine rather than the package. A generated page loses its script to an editor preview and to a `file://` open, so the link is the delivery rather than a convenience, and every generated surface here reaches a reader through one. A teach lesson's stylesheet is embedded rather than linked, so only its script still needs a server. It binds `127.0.0.1` and never a wildcard, because what it is pointed at is routinely a gitignored record tree. It sends `cache-control: no-store`, since a preview exists to be edited and reloaded and a cached stylesheet reads as a fix that did not work.
|
|
92
92
|
|
package/docs/agents/tasks.md
CHANGED
|
@@ -92,7 +92,7 @@ A relative path resolves against the project root first and against `.canon/task
|
|
|
92
92
|
|
|
93
93
|
The record carries `launchable` and `open`, the questions still waiting, each with the `label` that names it and the `why` its suggestion gave for needing a person. Exit codes: `0` launchable, `1` refused with `no-plan`, `archived`, or `bad-input`, `2` waiting on the operator.
|
|
94
94
|
|
|
95
|
-
A blank `- Answer:` is not a waiting question. The plan standard fixes an empty slot as accepting the `- Suggested:` line above it, so the
|
|
95
|
+
A blank `- Answer:` is not a waiting question. The plan standard fixes an empty slot as accepting the `- Suggested:` line above it, so the shapes this reads are `- Suggested: needs your call, <why>` and the two demonstrated paraphrases, `needs operator's call` and `needs the operator's call`, over an empty slot, which is what that standard writes where the answer turns on preference rather than on a technical default. A verb reading every blank slot as open would report every plan in the folder.
|
|
96
96
|
|
|
97
97
|
The question block is read through the same parser `canon tasks validate` runs, so the gate and the conformance check cannot drift into disagreeing about what a question is. A question carrying no suggestion at all is that check's finding rather than this one's, and it goes unread here.
|
|
98
98
|
|
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.53.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -40,7 +40,12 @@
|
|
|
40
40
|
"snapshot": "./scripts/core/snapshot.sh",
|
|
41
41
|
"bootstrap": "./scripts/core/bootstrap.sh",
|
|
42
42
|
"canon:sandbox": "canon sandbox",
|
|
43
|
-
"canon:sandbox:reset": "canon sandbox reset"
|
|
43
|
+
"canon:sandbox:reset": "canon sandbox reset",
|
|
44
|
+
"web:tokens": "canon 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
|
+
"web:dev": "cd web && astro dev",
|
|
46
|
+
"web:build": "bun run web:tokens && cd web && astro check && astro build",
|
|
47
|
+
"web:preview": "cd web && astro preview",
|
|
48
|
+
"web:e2e": "cd web && playwright test"
|
|
44
49
|
},
|
|
45
50
|
"dependencies": {
|
|
46
51
|
"commander": "^13.1.0",
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
"cspell": "^8.17.1",
|
|
62
67
|
"husky": "^9.1.7",
|
|
63
68
|
"prettier": "^3.8.1",
|
|
69
|
+
"prettier-plugin-astro": "^0.14.1",
|
|
64
70
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
65
71
|
"typescript": "^5.9.3",
|
|
66
72
|
"vitest": "^4.1.5"
|
package/src/commands/labels.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface ScanOptions {
|
|
|
17
17
|
readonly event?: string
|
|
18
18
|
readonly title?: string
|
|
19
19
|
readonly body?: string
|
|
20
|
+
readonly bodyFile?: string
|
|
20
21
|
readonly head?: string
|
|
21
22
|
readonly json?: boolean
|
|
22
23
|
}
|
|
@@ -100,6 +101,10 @@ export function register(program: Command): void {
|
|
|
100
101
|
)
|
|
101
102
|
.option('--title <text>', 'Title to scan, overriding the event payload')
|
|
102
103
|
.option('--body <text>', 'Body to scan, overriding the event payload')
|
|
104
|
+
.option(
|
|
105
|
+
'--body-file <path>',
|
|
106
|
+
'Body to scan, read from a file, overriding the event payload',
|
|
107
|
+
)
|
|
103
108
|
.option('--head <ref>', 'Head branch, overriding the event payload')
|
|
104
109
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
105
110
|
.addHelpText(
|
|
@@ -146,6 +151,7 @@ export function register(program: Command): void {
|
|
|
146
151
|
'Examples:',
|
|
147
152
|
' canon labels scan --event "$GITHUB_EVENT_PATH"',
|
|
148
153
|
' canon labels scan --title "feat: x" --body "planned under v68.5" --head feat/x',
|
|
154
|
+
' canon labels scan --body-file reply.md',
|
|
149
155
|
'',
|
|
150
156
|
].join('\n'),
|
|
151
157
|
)
|
package/src/commands/tasks.ts
CHANGED
|
@@ -209,9 +209,11 @@ export function register(program: Command): void {
|
|
|
209
209
|
' 2 the plan waits on the operator, and open names every slot',
|
|
210
210
|
'',
|
|
211
211
|
'A blank Answer accepts the Suggested line above it, so only',
|
|
212
|
-
'`- Suggested: needs your call, <why>`
|
|
213
|
-
'
|
|
214
|
-
'
|
|
212
|
+
'`- Suggested: needs your call, <why>` and its two demonstrated',
|
|
213
|
+
"paraphrases, needs operator's call and needs the operator's call,",
|
|
214
|
+
'over an empty slot are a stop. It reports and never writes.',
|
|
215
|
+
'Branch on launchable rather than on the exit code, which a shell',
|
|
216
|
+
'function wrapping canon can flatten to zero.',
|
|
215
217
|
'',
|
|
216
218
|
'A relative path resolves against the project root first and against',
|
|
217
219
|
'.canon/tasks/ second, so the ../plans/ link a board row writes works.',
|
package/src/labels/event.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type ScanInputRefusal =
|
|
|
6
6
|
| 'unreadable-event'
|
|
7
7
|
| 'not-a-pull-request'
|
|
8
8
|
| 'unreadable-review'
|
|
9
|
+
| 'conflicting-body-input'
|
|
10
|
+
| 'unreadable-body-file'
|
|
9
11
|
|
|
10
12
|
export type ResolvedScanInput =
|
|
11
13
|
| {
|
|
@@ -26,6 +28,7 @@ export interface ScanInputOptions {
|
|
|
26
28
|
readonly event?: string
|
|
27
29
|
readonly title?: string
|
|
28
30
|
readonly body?: string
|
|
31
|
+
readonly bodyFile?: string
|
|
29
32
|
readonly head?: string
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -46,6 +49,27 @@ export function resolveScanInput(opts: ScanInputOptions): ResolvedScanInput {
|
|
|
46
49
|
let headRefName = opts.head
|
|
47
50
|
let source: 'pull-request' | 'review' = 'pull-request'
|
|
48
51
|
|
|
52
|
+
if (opts.bodyFile !== undefined) {
|
|
53
|
+
if (body !== undefined) {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'refused',
|
|
56
|
+
reason: 'conflicting-body-input',
|
|
57
|
+
message:
|
|
58
|
+
'--body and --body-file cannot both be given, since only one text can be scanned.',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
body = readFileSync(opts.bodyFile, 'utf8')
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'refused',
|
|
67
|
+
reason: 'unreadable-body-file',
|
|
68
|
+
message: `${opts.bodyFile} could not be read, so no body was there to scan.`,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
if (opts.event !== undefined) {
|
|
50
74
|
let raw: string
|
|
51
75
|
try {
|
package/src/records/validate.ts
CHANGED
|
@@ -72,6 +72,7 @@ export const FINDING_KINDS = [
|
|
|
72
72
|
'closing-partial',
|
|
73
73
|
'item-incomplete',
|
|
74
74
|
'category-mismatch',
|
|
75
|
+
'operator-call-phrasing',
|
|
75
76
|
] as const
|
|
76
77
|
|
|
77
78
|
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
@@ -157,6 +158,53 @@ export function isSharedScratch(kind: RecordKind): boolean {
|
|
|
157
158
|
const NONE_IDENTIFIED = 'None identified.'
|
|
158
159
|
const NUMBERED_FILE = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The suggestion the plan standard fixes for a question that turns on the
|
|
163
|
+
* operator's preference rather than on a technical default. `src/tasks/answers.ts`
|
|
164
|
+
* reads this alongside `normalizeOperatorCall` so the dispatch gate and this
|
|
165
|
+
* write-time check cannot drift into recognizing different spellings.
|
|
166
|
+
*/
|
|
167
|
+
export const OPERATOR_CALL = 'needs your call'
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The two demonstrated paraphrases of `OPERATOR_CALL`, longer variant first so
|
|
171
|
+
* it is not read as a shorter match sitting inside it: `operator's call` is a
|
|
172
|
+
* substring of `the operator's call`, and checking it first would rewrite the
|
|
173
|
+
* `the` into place with the wrong phrase on either side.
|
|
174
|
+
*/
|
|
175
|
+
const OPERATOR_CALL_PHRASES = [
|
|
176
|
+
"the operator's call",
|
|
177
|
+
"operator's call",
|
|
178
|
+
] as const
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reads a recognized paraphrase as the canonical phrase, so a caller testing
|
|
182
|
+
* `startsWith(OPERATOR_CALL)` sees one spelling regardless of which wording the
|
|
183
|
+
* author used. Case-insensitive, since the corpus capitalizes a suggestion's
|
|
184
|
+
* first word, and it rewrites only the matched span so the reason text either
|
|
185
|
+
* side of it survives untouched.
|
|
186
|
+
*
|
|
187
|
+
* Short-circuits on an already-canonical opening before scanning the rest of
|
|
188
|
+
* the text. Without that, a suggestion already opening with `needs your call`
|
|
189
|
+
* that goes on to mention a paraphrase inside its own reason, such as
|
|
190
|
+
* `needs your call, since the operator's call outranks a default`, has that
|
|
191
|
+
* later occurrence rewritten too, which garbles the reason the dispatcher
|
|
192
|
+
* reports rather than leaving it as the author wrote it.
|
|
193
|
+
*/
|
|
194
|
+
export function normalizeOperatorCall(text: string): string {
|
|
195
|
+
const lower = text.toLowerCase()
|
|
196
|
+
if (lower.startsWith(OPERATOR_CALL)) return text
|
|
197
|
+
|
|
198
|
+
for (const phrase of OPERATOR_CALL_PHRASES) {
|
|
199
|
+
const at = lower.indexOf(phrase)
|
|
200
|
+
if (at !== -1) {
|
|
201
|
+
return `${text.slice(0, at)}your call${text.slice(at + phrase.length)}`
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return text
|
|
206
|
+
}
|
|
207
|
+
|
|
160
208
|
function finding(
|
|
161
209
|
kind: FindingKind,
|
|
162
210
|
record: string,
|
|
@@ -295,6 +343,9 @@ function shorten(label: string): string {
|
|
|
295
343
|
return label.length > 60 ? `${label.slice(0, 57)}...` : label
|
|
296
344
|
}
|
|
297
345
|
|
|
346
|
+
const SUGGESTED_PREFIX = '- Suggested:'
|
|
347
|
+
const ANSWER_PREFIX = '- Answer:'
|
|
348
|
+
|
|
298
349
|
function checkQuestionContract(name: string, lines: string[]): Finding[] {
|
|
299
350
|
if (lines.some((line) => line.trim() === NONE_IDENTIFIED)) return []
|
|
300
351
|
|
|
@@ -302,8 +353,12 @@ function checkQuestionContract(name: string, lines: string[]): Finding[] {
|
|
|
302
353
|
|
|
303
354
|
for (const question of readQuestions(lines)) {
|
|
304
355
|
const subject = shorten(question.label)
|
|
356
|
+
const suggested = question.body.find((line) =>
|
|
357
|
+
line.startsWith(SUGGESTED_PREFIX),
|
|
358
|
+
)
|
|
359
|
+
const answer = question.body.find((line) => line.startsWith(ANSWER_PREFIX))
|
|
305
360
|
|
|
306
|
-
if (!
|
|
361
|
+
if (!suggested) {
|
|
307
362
|
findings.push(
|
|
308
363
|
finding(
|
|
309
364
|
'suggestion-missing',
|
|
@@ -314,7 +369,7 @@ function checkQuestionContract(name: string, lines: string[]): Finding[] {
|
|
|
314
369
|
)
|
|
315
370
|
}
|
|
316
371
|
|
|
317
|
-
if (!
|
|
372
|
+
if (!answer) {
|
|
318
373
|
findings.push(
|
|
319
374
|
finding(
|
|
320
375
|
'question-unanswerable',
|
|
@@ -324,6 +379,29 @@ function checkQuestionContract(name: string, lines: string[]): Finding[] {
|
|
|
324
379
|
),
|
|
325
380
|
)
|
|
326
381
|
}
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
suggested &&
|
|
385
|
+
answer &&
|
|
386
|
+
answer.slice(ANSWER_PREFIX.length).trim() === ''
|
|
387
|
+
) {
|
|
388
|
+
const text = suggested.slice(SUGGESTED_PREFIX.length).trim()
|
|
389
|
+
const normalized = normalizeOperatorCall(text)
|
|
390
|
+
|
|
391
|
+
if (
|
|
392
|
+
normalized.toLowerCase().startsWith(OPERATOR_CALL) &&
|
|
393
|
+
!text.toLowerCase().startsWith(OPERATOR_CALL)
|
|
394
|
+
) {
|
|
395
|
+
findings.push(
|
|
396
|
+
finding(
|
|
397
|
+
'operator-call-phrasing',
|
|
398
|
+
name,
|
|
399
|
+
subject,
|
|
400
|
+
'defers to the operator over a blank Answer without opening with the canonical needs your call, so a paraphrase reaches the corpus instead of the fixed spelling.',
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
327
405
|
}
|
|
328
406
|
|
|
329
407
|
return findings
|
package/src/tasks/answers.ts
CHANGED
|
@@ -3,20 +3,17 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { isAbsolute, relative, resolve } from 'node:path'
|
|
4
4
|
import { isUnder } from '@/paths'
|
|
5
5
|
import { recordDir, recordDirs } from '@/record-root'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
normalizeOperatorCall,
|
|
8
|
+
OPERATOR_CALL,
|
|
9
|
+
readQuestions,
|
|
10
|
+
splitPlanSections,
|
|
11
|
+
} from '@/records/validate'
|
|
7
12
|
|
|
8
13
|
const PLANS = 'plans'
|
|
9
14
|
const TASKS = 'tasks'
|
|
10
15
|
const ARCHIVE = 'archive'
|
|
11
16
|
|
|
12
|
-
/**
|
|
13
|
-
* The suggestion the plan standard fixes for a question that turns on the
|
|
14
|
-
* operator's preference rather than on a technical default. Every other
|
|
15
|
-
* suggestion is accepted by a blank slot, so only this phrase over an empty
|
|
16
|
-
* `- Answer:` is a stop.
|
|
17
|
-
*/
|
|
18
|
-
const OPERATOR_CALL = 'needs your call'
|
|
19
|
-
|
|
20
17
|
const SUGGESTED_PREFIX = '- Suggested:'
|
|
21
18
|
const ANSWER_PREFIX = '- Answer:'
|
|
22
19
|
|
|
@@ -112,9 +109,15 @@ function isAnswered(body: readonly string[]): boolean {
|
|
|
112
109
|
* behind a full stop, so both separators come off. Reporting the phrase with
|
|
113
110
|
* whatever punctuation followed it hands the operator a stray mark where the
|
|
114
111
|
* reason should start.
|
|
112
|
+
*
|
|
113
|
+
* Takes the normalized suggestion rather than the raw one, so the length
|
|
114
|
+
* stripped from the front is always `OPERATOR_CALL`'s own regardless of which
|
|
115
|
+
* recognized wording the author wrote. The `operator's call` and
|
|
116
|
+
* `the operator's call` variants read longer than `your call`, and slicing by
|
|
117
|
+
* the canonical length against the raw text would cut into the reason itself.
|
|
115
118
|
*/
|
|
116
|
-
function reasonOf(
|
|
117
|
-
const rest =
|
|
119
|
+
function reasonOf(normalized: string): string {
|
|
120
|
+
const rest = normalized.slice(OPERATOR_CALL.length).replace(/^[,.;:\s]+/, '')
|
|
118
121
|
|
|
119
122
|
return rest.length > 0 ? rest : 'no reason stated'
|
|
120
123
|
}
|
|
@@ -130,10 +133,13 @@ function openQuestions(lines: readonly string[]): OpenQuestion[] {
|
|
|
130
133
|
|
|
131
134
|
for (const question of readQuestions(lines)) {
|
|
132
135
|
const suggested = suggestionOf(question.body)
|
|
133
|
-
if (!suggested
|
|
136
|
+
if (!suggested) continue
|
|
137
|
+
|
|
138
|
+
const normalized = normalizeOperatorCall(suggested)
|
|
139
|
+
if (!normalized.toLowerCase().startsWith(OPERATOR_CALL)) continue
|
|
134
140
|
if (isAnswered(question.body)) continue
|
|
135
141
|
|
|
136
|
-
open.push({ label: question.label, why: reasonOf(
|
|
142
|
+
open.push({ label: question.label, why: reasonOf(normalized) })
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
return open
|
|
@@ -6,6 +6,7 @@ const baseURL = `http://localhost:${4321 + (Number(process.env.WORKTREE_PORT_OFF
|
|
|
6
6
|
export default defineConfig({
|
|
7
7
|
testDir: 'e2e',
|
|
8
8
|
forbidOnly: isCI,
|
|
9
|
+
// Absorbs shared-runner noise on a fresh scaffold with no flake history, at the cost of hiding a real defect until someone reads the run summary.
|
|
9
10
|
retries: isCI ? 2 : 0,
|
|
10
11
|
// No override here: Playwright's own CPU-derived default suits every CI runner
|
|
11
12
|
reporter: isCI ? 'list' : 'html',
|
|
@@ -19,7 +20,9 @@ export default defineConfig({
|
|
|
19
20
|
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
20
21
|
],
|
|
21
22
|
webServer: {
|
|
22
|
-
command:
|
|
23
|
+
command: process.env.DIST_PREBUILT
|
|
24
|
+
? 'bun run preview'
|
|
25
|
+
: 'bun run build && bun run preview',
|
|
23
26
|
url: baseURL,
|
|
24
27
|
reuseExistingServer: false,
|
|
25
28
|
env: { ASTRO_PREVIEW_BACKGROUND: '0' },
|
|
@@ -19,7 +19,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
19
19
|
|
|
20
20
|
- `astro.config.mjs`: `@astrojs/react` integration, `@tailwindcss/vite` in `vite.plugins`, `@/` path alias via `vite.resolve.alias`, `ASTRO_SITE` env for the `site` field. Port `4321` plus `WORKTREE_PORT_OFFSET` at `server.port`, with `strictPort` under `vite.server` and `vite.preview`. Astro merges the user's `vite` block into the config backing both its dev and its static preview server, and feeds `server.port` through as the preview port, so the port sits at the top level while the bind guarantee sits under `vite`.
|
|
21
21
|
- `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
|
|
22
|
-
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`.
|
|
22
|
+
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
|
|
23
23
|
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
|
|
24
24
|
- `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). `src/pages/**` exempt from filename and folder naming conventions because Astro's file-based routing ties names to URL segments.
|
|
25
25
|
|
|
@@ -6,6 +6,7 @@ const baseURL = `http://localhost:${5173 + (Number(process.env.WORKTREE_PORT_OFF
|
|
|
6
6
|
export default defineConfig({
|
|
7
7
|
testDir: 'e2e',
|
|
8
8
|
forbidOnly: isCI,
|
|
9
|
+
// Absorbs shared-runner noise on a fresh scaffold with no flake history, at the cost of hiding a real defect until someone reads the run summary.
|
|
9
10
|
retries: isCI ? 2 : 0,
|
|
10
11
|
// No override here: Playwright's own CPU-derived default suits every CI runner
|
|
11
12
|
reporter: isCI ? 'list' : 'html',
|
|
@@ -5,6 +5,10 @@ on:
|
|
|
5
5
|
branches: [main]
|
|
6
6
|
workflow_dispatch:
|
|
7
7
|
|
|
8
|
+
concurrency:
|
|
9
|
+
group: verify-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
8
12
|
jobs:
|
|
9
13
|
static-checks:
|
|
10
14
|
name: š Static Checks
|
|
@@ -90,6 +94,13 @@ jobs:
|
|
|
90
94
|
- name: Build
|
|
91
95
|
run: bun run build
|
|
92
96
|
|
|
97
|
+
- name: Upload Build Output
|
|
98
|
+
uses: actions/upload-artifact@v4
|
|
99
|
+
with:
|
|
100
|
+
name: build-output
|
|
101
|
+
path: dist/
|
|
102
|
+
retention-days: 1
|
|
103
|
+
|
|
93
104
|
e2e-tests:
|
|
94
105
|
name: š E2E Tests
|
|
95
106
|
runs-on: ubuntu-latest
|
|
@@ -111,18 +122,29 @@ jobs:
|
|
|
111
122
|
- name: Install Dependencies
|
|
112
123
|
run: bun install --frozen-lockfile
|
|
113
124
|
|
|
125
|
+
- name: Download Build Output
|
|
126
|
+
uses: actions/download-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: build-output
|
|
129
|
+
path: dist/
|
|
130
|
+
|
|
131
|
+
- name: Install Playwright System Dependencies
|
|
132
|
+
run: bunx playwright install-deps chromium
|
|
133
|
+
|
|
114
134
|
- name: Cache Playwright Browsers
|
|
115
135
|
id: playwright-cache
|
|
116
136
|
uses: actions/cache@v4
|
|
117
137
|
with:
|
|
118
138
|
path: ~/.cache/ms-playwright
|
|
119
|
-
key: playwright-${{ runner.os }}-${{ hashFiles('**/package.json') }}
|
|
139
|
+
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('**/package.json') }}
|
|
120
140
|
|
|
121
141
|
- name: Install Playwright Chromium
|
|
122
142
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
123
143
|
run: bunx playwright install chromium
|
|
124
144
|
|
|
125
145
|
- name: Run E2E Tests
|
|
146
|
+
env:
|
|
147
|
+
DIST_PREBUILT: 'true'
|
|
126
148
|
run: bun run test:e2e --project=chromium
|
|
127
149
|
|
|
128
150
|
- name: Upload E2E Report
|
|
@@ -24,10 +24,29 @@ const CASES: CaptureCase[] = [
|
|
|
24
24
|
},
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
+
const args = process.argv.slice(2)
|
|
28
|
+
const checkConsoleClean = args.includes('--check-console-clean')
|
|
29
|
+
const requireBaseUrl = args.includes('--require-base-url')
|
|
30
|
+
|
|
31
|
+
if (requireBaseUrl && !process.env.SCREENSHOT_BASE_URL) {
|
|
32
|
+
console.error('SCREENSHOT_BASE_URL is required with --require-base-url')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
const BASE_URL = process.env.SCREENSHOT_BASE_URL ?? 'http://localhost:4173'
|
|
28
|
-
|
|
37
|
+
|
|
38
|
+
let hostname: string
|
|
39
|
+
try {
|
|
40
|
+
hostname = new URL(BASE_URL).hostname
|
|
41
|
+
} catch {
|
|
42
|
+
console.error(`SCREENSHOT_BASE_URL is not a valid URL: ${BASE_URL}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const OUT_DIR = path.join('screenshots', hostname)
|
|
29
47
|
|
|
30
48
|
const browser = await chromium.launch()
|
|
49
|
+
const consoleErrors: string[] = []
|
|
31
50
|
|
|
32
51
|
for (const captureCase of CASES) {
|
|
33
52
|
const context = await browser.newContext({
|
|
@@ -35,6 +54,16 @@ for (const captureCase of CASES) {
|
|
|
35
54
|
})
|
|
36
55
|
const page = await context.newPage()
|
|
37
56
|
|
|
57
|
+
if (checkConsoleClean) {
|
|
58
|
+
page.on('console', (msg) => {
|
|
59
|
+
if (msg.type() === 'error') {
|
|
60
|
+
consoleErrors.push(
|
|
61
|
+
`${captureCase.section}/${captureCase.theme}: ${msg.text()}`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
if (captureCase.setup) await captureCase.setup(page)
|
|
39
68
|
|
|
40
69
|
await page.goto(`${BASE_URL}${captureCase.route}`)
|
|
@@ -51,3 +80,9 @@ for (const captureCase of CASES) {
|
|
|
51
80
|
}
|
|
52
81
|
|
|
53
82
|
await browser.close()
|
|
83
|
+
|
|
84
|
+
if (checkConsoleClean && consoleErrors.length > 0) {
|
|
85
|
+
console.error('console errors detected:')
|
|
86
|
+
for (const error of consoleErrors) console.error(` ${error}`)
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
@@ -47,6 +47,7 @@ packages = [
|
|
|
47
47
|
"test:e2e:ui" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && playwright test --ui"
|
|
48
48
|
"test:e2e:report" = "playwright show-report"
|
|
49
49
|
"check:full" = "./scripts/verify.sh && bun run test:e2e"
|
|
50
|
+
"smoke:prod" = "bun e2e/screenshot.ts --check-console-clean --require-base-url"
|
|
50
51
|
|
|
51
52
|
[scripts.override]
|
|
52
53
|
"lint" = "eslint . --max-warnings 0"
|
|
@@ -55,5 +56,5 @@ packages = [
|
|
|
55
56
|
[gitignore]
|
|
56
57
|
"# Build" = ["dist/"]
|
|
57
58
|
"# Coverage" = ["coverage/"]
|
|
58
|
-
"# Playwright" = ["test-results/", "playwright-report/", "blob-report/", "playwright/.cache/"
|
|
59
|
+
"# Playwright" = ["test-results/", "playwright-report/", "blob-report/", "playwright/.cache/"]
|
|
59
60
|
"# VSCode" = [".vscode/*", "!.vscode/extensions.json", "!.vscode/settings.json"]
|
package/tooling/web/reference.md
CHANGED
|
@@ -12,7 +12,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
12
12
|
|
|
13
13
|
- `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
|
|
14
14
|
- `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
|
|
15
|
-
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and its own viewport, and the loop below writes `screenshots/<section>/<theme>.png
|
|
15
|
+
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and its own viewport, and the loop below writes `screenshots/<hostname>/<section>/<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. Per-project cases extend the one record. A route's themes sit together under its section folder, so the filename carries the theme alone. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
|
|
16
16
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
17
17
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
18
18
|
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
|
|
@@ -106,11 +106,20 @@ Append rows:
|
|
|
106
106
|
| `bun run test:e2e` | Run Playwright E2E tests. |
|
|
107
107
|
| `bun run test:e2e:changed` | Run Playwright E2E tests for specs the import graph reaches from the current diff. |
|
|
108
108
|
| `bun run screenshot` | Build, preview, then capture screenshots. |
|
|
109
|
+
| `bun run smoke:prod` | Capture against `SCREENSHOT_BASE_URL`, requiring it set and failing on any console error. |
|
|
109
110
|
|
|
110
111
|
`canon tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
|
|
111
112
|
|
|
112
113
|
`governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a route changes. It fires on route and page files rather than on every component, so a shared component changing every screen fires nothing and the operator runs the capture by hand.
|
|
113
114
|
|
|
115
|
+
The screenshot output now tracks in git rather than getting discarded, so the first capture a scaffolded target runs after this change is the baseline it commits.
|
|
116
|
+
|
|
117
|
+
## Gitignore (extend)
|
|
118
|
+
|
|
119
|
+
`[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
|
|
120
|
+
|
|
121
|
+
- `"# Playwright" = ["test-results/", "playwright-report/", "blob-report/", "playwright/.cache/"]`
|
|
122
|
+
|
|
114
123
|
## Verify script
|
|
115
124
|
|
|
116
125
|
The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build. Stack adapters may override if their typecheck or build differs.
|