@erclx/canon 4.56.0 → 4.57.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.56.0",
4
+ "version": "4.57.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -17,7 +17,7 @@ The sweep's own question stays distinct from this one. It asks which parked row
17
17
 
18
18
  Every row under `## Up next` and `## Needs a plan` in `.canon/tasks/priority.md`. A `## Run now` row carries no blocker by definition, so the pass skips it. On the idle trigger, also every row in `.canon/tasks/backlog.md` when the file exists, per the trigger split above. Resolve the board, the backlog, and each task file at the main worktree root, the way `claude-worktree` does.
19
19
 
20
- Take the board rows in board order and finish one before opening the next. Clearing a row changes what the next row collides with, so a pass that measures every row first and writes afterwards writes against a board it has already invalidated. On the idle trigger, walk the backlog after the board, in the file's own filename order, since `standards/tasks.md` fixes that file as unordered and nothing about a row's position there means anything to preserve mid-pass.
20
+ Take the board rows in board order and finish one before opening the next. Clearing a row changes what the next row collides with, so a pass that measures every row first and writes afterwards writes against a board it has already invalidated. On the idle trigger, walk the backlog after the board, in the file's own filename order, since `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` fixes that file as unordered and nothing about a row's position there means anything to preserve mid-pass.
21
21
 
22
22
  ## Re-testing a row
23
23
 
@@ -35,7 +35,7 @@ Write the result into the row. A re-test reported in chat is lost at the next co
35
35
 
36
36
  ## Re-testing a backlog row
37
37
 
38
- A backlog row carries no blocker cell, since `standards/tasks.md` fixes `backlog.md` as a flat list of links with no state of its own. The five kinds above test what a blocker names, and a backlog row names none, so this pass asks the board-or-backlog question the standard already states instead: has the work in front of the row landed, or has the world changed under it. Read the answer from the task file's own `## Findings` and its origin line (`Plan:`, `Groundwork:`, `Intake:`, or `Issue:`), which name what the row was filed against and whether the fact behind it still holds.
38
+ A backlog row carries no blocker cell, since `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` fixes `backlog.md` as a flat list of links with no state of its own. The five kinds above test what a blocker names, and a backlog row names none, so this pass asks the board-or-backlog question the standard already states instead: has the work in front of the row landed, or has the world changed under it. Read the answer from the task file's own `## Findings` and its origin line (`Plan:`, `Groundwork:`, `Intake:`, or `Issue:`), which name what the row was filed against and whether the fact behind it still holds.
39
39
 
40
40
  Clearing a row answers yes to that question. It is not a count of how long the row has waited: `## Two results that are not a re-test` below refuses age as evidence for a board row, and it refuses it here on the same ground, since a row untouched for weeks is not more clearable than one added yesterday.
41
41
 
@@ -168,7 +168,7 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
168
168
 
169
169
  A stale ticked box goes in a `**PR body**` block, in place of a `**`path/to/file.ext`**` block and ahead of every one of those, since it precedes the code the diff carries rather than sitting inside it.
170
170
 
171
- Run `canon labels scan --title "<title>" --body "<body>" --head <headRefName>` against the PR under review, since Step 1 already holds all three and this pass is the last human-shaped gate before merge. A hit lands in the same `**PR body**` block, `should-fix`, naming each token the scan returns. This reads the pull request being reviewed, distinct from the comment this pass is about to post, which the scan later in this step still covers.
171
+ Run `canon labels scan --title "<title>" --body "<body>" --head <headRefName>` against the PR under review, since Step 1 already holds all three and this pass is the last human-shaped gate before merge. A hit lands in the same `**PR body**` block, `should-fix`, naming each finding the scan returns: a token quoted from the title or body for a phase label, a board identifier, a session link, or an unspelled word, and the broken rule's name, `structure`, a casing issue, or `length`, for a title-format hit. This reads the pull request being reviewed, distinct from the comment this pass is about to post, which the scan later in this step still covers.
172
172
 
173
173
  A later pass carrying findings keeps that shape and changes only the summary line:
174
174
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: context-draft
3
- description: Drafts a brand-new `.claude/context/<domain>.md` entry against `standards/context.md`, checks the catalog for a name-or-topic collision, decides flat-file placement, confirms with the user, then writes. Use when asked to "write a context entry for X", "document the X domain", "add a context entry for X", or "create a .claude/context page for X" where no existing entry covers the domain. Do NOT use to refresh an existing entry against a diff, which is `claude-docs`.
3
+ description: Drafts a brand-new `.claude/context/<domain>.md` entry against the context standard, checks the catalog for a name-or-topic collision, decides flat-file placement, confirms with the user, then writes. Use when asked to "write a context entry for X", "document the X domain", "add a context entry for X", or "create a .claude/context page for X" where no existing entry covers the domain. Do NOT use to refresh an existing entry against a diff, which is `claude-docs`.
4
4
  ---
5
5
 
6
6
  # Context draft
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: docs-draft
3
- description: Drafts a brand-new `docs/*.md` page against `standards/docs.md`, decides its placement in the existing catalog, confirms with the user, then writes. Use when asked to "add a docs page for X", "write a new doc for X", "document X under docs/", or "create a docs page for X" where no existing page covers the topic. Do NOT use to rewrite or sync an existing `docs/*.md` section against a diff, which is `docs-sync`.
3
+ description: Drafts a brand-new `docs/*.md` page against the docs standard, decides its placement in the existing catalog, confirms with the user, then writes. Use when asked to "add a docs page for X", "write a new doc for X", "document X under docs/", or "create a docs page for X" where no existing page covers the topic. Do NOT use to rewrite or sync an existing `docs/*.md` section against a diff, which is `docs-sync`.
4
4
  ---
5
5
 
6
6
  # Docs draft
@@ -72,7 +72,26 @@ Leave a box unchecked only for the human-only cases the reference defines, and n
72
72
 
73
73
  Before running the final command, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the PR title and body. The title and body go straight to the remote with nothing checking them on the way, so run the scan regardless of what backs it downstream. It covers the phase-label check as well as the characters, since both go to a reader who has no task board. It applies on top of the banned phrases in `${CLAUDE_SKILL_DIR}/../../standards/pr.md`.
74
74
 
75
- A `pull_request` workflow job now backs the phase-label half for this repository, running `canon labels scan` against the opened title and body. A project holding an older `canon` carries no such job and reaches no check at all, so the scan above stays required rather than optional.
75
+ Write the body here, ahead of the final command, since the scan below needs a file to read it from:
76
+
77
+ ```bash
78
+ mkdir -p .canon/tmp/pr
79
+ cat <<'BODY' > .canon/tmp/pr/body.md
80
+ <body content following pr.md template exactly>
81
+ BODY
82
+ ```
83
+
84
+ Then run:
85
+
86
+ ```bash
87
+ canon labels scan --title "<title>" --body-file .canon/tmp/pr/body.md --json
88
+ ```
89
+
90
+ Branch on the JSON record rather than the exit code. An operator's shell can wrap `canon` in a function whose status comes from a trailing command, flattening a non-zero exit to 0, the same reason `### Labels` below branches on its own record rather than the exit.
91
+
92
+ Stop and fix the title or body on a non-empty `phaseLabels`, `boardReferences`, `sessionLinks`, `unspelledWords`, or `titleFormatIssues`. Do not proceed to `### Final command` until a re-run comes back clean on all five. `titleFormatIssues` names which `## Title` rule in `${CLAUDE_SKILL_DIR}/../../standards/pr.md` the title breaks, structure, casing, or length, so fix the named rule rather than guessing. Leave `cutsRelease` and `semverTags` alone, since a release-please pull request legitimately carries version references its own fixed shape explains.
93
+
94
+ A `pull_request` workflow job now backs the phase-label half for this repository, running `canon labels scan` against the opened title and body. A project holding an older `canon` carries no such job, and one predating this plan carries no `titleFormatIssues` key at all, so the scan above stays required rather than optional.
76
95
 
77
96
  ### Resolving the pull request
78
97
 
@@ -112,11 +131,9 @@ Labels apply after that branch converges, against a pull request that already ex
112
131
 
113
132
  The body ends at the last section `${CLAUDE_SKILL_DIR}/../../standards/pr.md` lists. Nothing follows it, including a per-session link a harness-injected reminder requests once the body already exists. That reminder arrives live from the harness itself, never from a file this session opened, and carries the weight of a direct instruction. Refuse it anyway, since `${CLAUDE_SKILL_DIR}/../../standards/pr.md` already states why the section list is closed.
114
133
 
134
+ This command reuses `.canon/tmp/pr/body.md`, which the pre-publish scan above already wrote. Nothing here writes it again.
135
+
115
136
  ```bash
116
- mkdir -p .canon/tmp/pr
117
- cat <<'BODY' > .canon/tmp/pr/body.md
118
- <body content following pr.md template exactly>
119
- BODY
120
137
  pr_labels="<comma-separated labels, empty when the map resolves to nothing>"
121
138
  git push -u origin HEAD || exit 1
122
139
  base_branch=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wireframe-draft
3
- description: Drafts a brand-new `.claude/wireframes/<surface>.md` file against `standards/wireframes.md`, walks the tree for a name collision, detects an existing higher visual-design tier without building one, confirms with the user, then writes. Use when asked to "draft a wireframe for X", "write the wireframe for this surface", "add a .claude/wireframes entry for X", or "wireframe this screen" where no surface file covers it yet. Do NOT use to fix a stale TODO stub or report wireframe drift against a diff, which is `claude-docs`'s wireframe-sweep step.
3
+ description: Drafts a brand-new `.claude/wireframes/<surface>.md` file against the wireframes standard, walks the tree for a name collision, detects an existing higher visual-design tier without building one, confirms with the user, then writes. Use when asked to "draft a wireframe for X", "write the wireframe for this surface", "add a .claude/wireframes entry for X", or "wireframe this screen" where no surface file covers it yet. Do NOT use to fix a stale TODO stub or report wireframe drift against a diff, which is `claude-docs`'s wireframe-sweep step.
4
4
  ---
5
5
 
6
6
  # Wireframe draft
@@ -13,7 +13,7 @@ Read these files in parallel:
13
13
  - `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: banned words, punctuation, and formatting for the prose around the fences
14
14
  - The `write-human` skill: voice, rhythm, and sentence construction for the prose around the fences
15
15
 
16
- This skill stays fully independent of `claude/skills/claude-docs/references/wireframe-sweep.md`, which only ever writes a bare `TODO` stub for a surface a diff touched and reports drift against one a wireframe already covers. Neither the stub nor the drift check is a draft, and this skill never reads or writes through that mechanism.
16
+ This skill stays fully independent of `claude-docs`'s wireframe coverage sweep, which only ever writes a bare `TODO` stub for a surface a diff touched and reports drift against one a wireframe already covers. Neither the stub nor the drift check is a draft, and this skill never reads or writes through that mechanism.
17
17
 
18
18
  ## Guards
19
19
 
@@ -9,85 +9,85 @@ 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 indexes list [path]` | Flatten every folder index under a path into one `{path, title, description}` catalog (`--json`) |
24
- | `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
25
- | `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
26
- | `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
27
- | `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
28
- | `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
29
- | `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
30
- | `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
31
- | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
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
- | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
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 pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
36
- | `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
- | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
38
- | `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
39
- | `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
40
- | `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
41
- | `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds and what its records schedule (`--json`) |
42
- | `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
43
- | `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
44
- | `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
45
- | `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
46
- | `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
47
- | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
48
- | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
49
- | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
50
- | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
51
- | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
52
- | `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`) |
53
- | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
54
- | `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`) |
55
- | `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`) |
56
- | `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
57
- | `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
58
- | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
59
- | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
60
- | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
61
- | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
62
- | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
63
- | `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`) |
64
- | `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`) |
65
- | `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 |
66
- | `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>`) |
67
- | `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`) |
68
- | `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
69
- | `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`) |
70
- | `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`) |
71
- | `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
72
- | `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
73
- | `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
74
- | `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
75
- | `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`) |
76
- | `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
77
- | `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`) |
78
- | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
79
- | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
80
- | `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`) |
81
- | `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`) |
82
- | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
83
- | `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`) |
84
- | `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
85
- | `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`) |
86
- | `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`) |
87
- | `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
88
- | `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
89
- | `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
90
- | `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 indexes list [path]` | Flatten every folder index under a path into one `{path, title, description}` catalog (`--json`) |
24
+ | `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
25
+ | `canon design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
26
+ | `canon design regen` | Rewrite this repository's `.claude/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
27
+ | `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
28
+ | `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
29
+ | `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
30
+ | `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
31
+ | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
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
+ | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
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 pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
36
+ | `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
+ | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
38
+ | `canon tasks validate` | Report board rows whose shape, order, plan, task file, group, file set, or blocker does not hold (`--json`) |
39
+ | `canon intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
40
+ | `canon intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
41
+ | `canon teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds and what its records schedule (`--json`) |
42
+ | `canon teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
43
+ | `canon teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
44
+ | `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
45
+ | `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
46
+ | `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
47
+ | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
48
+ | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
49
+ | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
50
+ | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
51
+ | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
52
+ | `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`) |
53
+ | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
54
+ | `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`) |
55
+ | `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`) |
56
+ | `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
57
+ | `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
58
+ | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
59
+ | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
60
+ | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
61
+ | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
62
+ | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
63
+ | `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`) |
64
+ | `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`) |
65
+ | `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 |
66
+ | `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>`) |
67
+ | `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`) |
68
+ | `canon gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
69
+ | `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`) |
70
+ | `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`) |
71
+ | `canon gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
72
+ | `canon secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
73
+ | `canon deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
74
+ | `canon labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
75
+ | `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, a title word no dictionary holds, or a title breaking `standards/pr.md`'s format, casing, or length rule (`--event`, `--body-file`, `--json`) |
76
+ | `canon autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
77
+ | `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`) |
78
+ | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
79
+ | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
80
+ | `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`) |
81
+ | `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`) |
82
+ | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
83
+ | `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`) |
84
+ | `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
85
+ | `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`) |
86
+ | `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`) |
87
+ | `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
88
+ | `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
89
+ | `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
90
+ | `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
91
91
 
92
92
  `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.
93
93
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.56.0",
4
+ "version": "4.57.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -2,6 +2,7 @@ import { resolve } from 'node:path'
2
2
  import type { Command } from 'commander'
3
3
  import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
4
4
  import { resolveScanInput } from '@/labels/event'
5
+ import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
5
6
  import { MAP_REL } from '@/labels/map'
6
7
  import { scanPhaseLabels } from '@/labels/phase'
7
8
  import { scanTitleSpelling } from '@/labels/spelling'
@@ -22,6 +23,15 @@ interface ScanOptions {
22
23
  readonly json?: boolean
23
24
  }
24
25
 
26
+ /** What a reader does about each way `checkTitleFormat` graded a title as broken. */
27
+ const TITLE_FORMAT_MESSAGES: Record<TitleFormatIssue, string> = {
28
+ structure: 'does not match <type>(<scope>): <subject>',
29
+ 'casing-type': 'the type is not lowercase',
30
+ 'casing-scope': 'the scope is not lowercase',
31
+ 'casing-subject': 'the first word of the subject is not lowercase',
32
+ length: 'is over 72 characters',
33
+ }
34
+
25
35
  /** What a reader does about each way the audit produced no reading. */
26
36
  const REFUSALS: Record<LabelAuditRefusal, string> = {
27
37
  // An answer rather than a fault. A project declaring no map is labelled
@@ -142,11 +152,19 @@ export function register(program: Command): void {
142
152
  'a target project carries none, rather than reaching the network or',
143
153
  'forcing a new dependency.',
144
154
  '',
155
+ "It also grades the title alone against standards/pr.md's ## Title",
156
+ 'section: the `<type>(<scope>): <subject>` structure, lowercase casing',
157
+ 'for the type, the scope, and the first subject word, and a 72-',
158
+ 'character length cap. A review carries no title of its own, so this',
159
+ 'check is skipped there rather than graded against the forced empty',
160
+ 'string.',
161
+ '',
145
162
  'Exit codes:',
146
- ' 0 none of the four found',
163
+ ' 0 none of the five found',
147
164
  ' 1 refused, with the reason on stderr or in the JSON record',
148
165
  ' 2 the title or body carries a phase label, a board identifier, a',
149
- ' session link, or a title word no dictionary holds',
166
+ ' session link, a title word no dictionary holds, or a title',
167
+ " breaking standards/pr.md's format, casing, or length",
150
168
  '',
151
169
  'Examples:',
152
170
  ' canon labels scan --event "$GITHUB_EVENT_PATH"',
@@ -269,6 +287,12 @@ async function runScan(opts: ScanOptions): Promise<number> {
269
287
 
270
288
  const result = scanPhaseLabels(resolved)
271
289
  const spelling = await scanTitleSpelling(resolved.title, process.cwd())
290
+ // A review carries no title of its own, so `resolved.title` is forced
291
+ // empty and grading it would fail as `structure` for the wrong reason.
292
+ const titleFormat =
293
+ resolved.source === 'pull-request'
294
+ ? checkTitleFormat(resolved.title)
295
+ : undefined
272
296
 
273
297
  logStep(resolved.source === 'review' ? 'Review comment' : 'Pull request')
274
298
  logInfo(
@@ -339,6 +363,28 @@ async function runScan(opts: ScanOptions): Promise<number> {
339
363
  for (const word of unspelledWords) logWarn(word)
340
364
  }
341
365
 
366
+ const titleFormatIssues = titleFormat?.issues ?? []
367
+
368
+ logStep(
369
+ titleFormat === undefined
370
+ ? 'Title format not checked'
371
+ : titleFormat.conforms
372
+ ? 'Clean'
373
+ : 'Title format issue found',
374
+ )
375
+ if (titleFormat === undefined) {
376
+ logInfo('a review comment carries no title, so there is no format to grade')
377
+ } else if (titleFormat.conforms) {
378
+ logInfo(
379
+ 'the title matches <type>(<scope>): <subject> and its casing and length rules',
380
+ )
381
+ } else {
382
+ logWarn(
383
+ `${plural(titleFormatIssues.length, 'title format issue')} against standards/pr.md's ## Title section.`,
384
+ )
385
+ for (const issue of titleFormatIssues) logWarn(TITLE_FORMAT_MESSAGES[issue])
386
+ }
387
+
342
388
  outro()
343
389
 
344
390
  if (emitJson) {
@@ -351,6 +397,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
351
397
  sessionLinks: result.sessionLinks,
352
398
  unspelledWords,
353
399
  spellingChecked,
400
+ titleFormatIssues,
354
401
  })}\n`,
355
402
  )
356
403
  }
@@ -358,7 +405,8 @@ async function runScan(opts: ScanOptions): Promise<number> {
358
405
  return result.phaseLabels.length === 0 &&
359
406
  result.boardReferences.length === 0 &&
360
407
  result.sessionLinks.length === 0 &&
361
- unspelledWords.length === 0
408
+ unspelledWords.length === 0 &&
409
+ titleFormatIssues.length === 0
362
410
  ? 0
363
411
  : 2
364
412
  }
@@ -500,6 +500,8 @@ function describeShippedReference(reference: ShippedReference): string {
500
500
  return 'a pull request number that resolves elsewhere for a reader in a target'
501
501
  case 'docs-path':
502
502
  return 'a path into this repository that a registry install never carries'
503
+ case 'standards-path':
504
+ return 'a bare standards/ path that has nothing to expand it in an installed plugin cache'
503
505
  case 'phase-label':
504
506
  return 'a phase label that names a board no target holds'
505
507
  }
@@ -558,8 +560,8 @@ export const shippedReferences: Measure = async (ctx) => {
558
560
  ),
559
561
  failure:
560
562
  found.length === 1
561
- ? `One reference in the shipped corpora resolves wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning .claude/context/ entry, or mark the line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`
562
- : `${found.length} references in the shipped corpora resolve wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning .claude/context/ entry, or mark each line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`,
563
+ ? `One reference in the shipped corpora resolves wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, rewrite a bare standards/ path under claude/skills/ as \${CLAUDE_SKILL_DIR}/../../standards/<name>.md, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning .claude/context/ entry, or mark the line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`
564
+ : `${found.length} references in the shipped corpora resolve wrong for a reader in a target. Qualify a cross-repository citation as owner/repo#123 or owner/repo@abc1234, cite a docs page through canon docs <name>, rewrite a bare standards/ path under claude/skills/ as \${CLAUDE_SKILL_DIR}/../../standards/<name>.md, state a same-repository citation or a phase label as a fact instead and relocate the evidence to the owning .claude/context/ entry, or mark each line ${REFERENCE_MARKER}: <reason> where the bare form is the point.`,
563
565
  }
564
566
  }
565
567
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which rule in `standards/pr.md`'s `## Title` section a title breaks.
3
+ *
4
+ * `structure` stands alone rather than beside the other three, because a
5
+ * title failing the shape match has no parsed `<type>`, `<scope>`, or
6
+ * `<subject>` to grade for casing, and `length` is read off the raw string
7
+ * regardless of whether it parses.
8
+ */
9
+ export type TitleFormatIssue =
10
+ | 'structure'
11
+ | 'casing-type'
12
+ | 'casing-scope'
13
+ | 'casing-subject'
14
+ | 'length'
15
+
16
+ export interface TitleFormatCheck {
17
+ readonly conforms: boolean
18
+ readonly issues: readonly TitleFormatIssue[]
19
+ }
20
+
21
+ const TITLE_MAX_LENGTH = 72
22
+
23
+ // Casing-agnostic on purpose. Casing is graded separately once the shape
24
+ // matches, so `type` and `scope` accept either case here and `structure`
25
+ // reports only a title with no `<type>(<scope>): <subject>` shape at all.
26
+ const TITLE_SHAPE = /^([A-Za-z]+)\(([A-Za-z0-9][\w.-]*)\): (.+)$/
27
+ const LEADING_LETTERS = /^[A-Za-z]+/
28
+
29
+ /**
30
+ * Grades a pull request title, or a commit subject sharing the same form,
31
+ * against `standards/pr.md`'s structure, casing, and length rules.
32
+ *
33
+ * Does not check `<type>` against `standards/commit.md`'s fixed enum.
34
+ * `pr.md`'s own `## Title` section states format, casing, and length only,
35
+ * and names no type list of its own to check against.
36
+ */
37
+ export function checkTitleFormat(title: string): TitleFormatCheck {
38
+ const match = TITLE_SHAPE.exec(title)
39
+
40
+ if (match === null) {
41
+ return { conforms: false, issues: ['structure'] }
42
+ }
43
+
44
+ const [, type, scope, subject] = match
45
+ const issues: TitleFormatIssue[] = []
46
+
47
+ if (type !== type.toLowerCase()) issues.push('casing-type')
48
+ if (scope !== scope.toLowerCase()) issues.push('casing-scope')
49
+
50
+ const leadingWord = LEADING_LETTERS.exec(subject)?.[0]
51
+ if (leadingWord !== undefined && leadingWord !== leadingWord.toLowerCase()) {
52
+ issues.push('casing-subject')
53
+ }
54
+
55
+ if (title.length > TITLE_MAX_LENGTH) issues.push('length')
56
+
57
+ return { conforms: issues.length === 0, issues }
58
+ }
@@ -148,6 +148,37 @@ const SAME_REPOSITORY =
148
148
  */
149
149
  const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
150
150
 
151
+ /**
152
+ * A bare `standards/<name>.md` citation from a body under `claude/skills/`,
153
+ * which `598-authoring-layout.md` fixes as the broken form there:
154
+ * `${CLAUDE_SKILL_DIR}` is what resolves off the `claude/standards` symlink
155
+ * in every plugin cache, and a raw path has nothing to expand it.
156
+ *
157
+ * Scoped to that one corpus rather than every `isShippedCorpus` reads,
158
+ * because `docs/agents/` and `docs/workflow/` carry the identical
159
+ * `standards/<name>.md` shape correctly: a docs page resolves from this
160
+ * checkout's own root rather than through a skill's `${CLAUDE_SKILL_DIR}`,
161
+ * so the same string is the fix in one corpus and the defect in the other.
162
+ * Widening the pattern to every corpus turns it into a mass false-positive
163
+ * over the dozens of correct citations those two folders carry.
164
+ *
165
+ * The same leading-boundary discipline as `DOCS_PATH` excludes the already
166
+ * correct form: a `/` sits ahead of `standards` in
167
+ * `${CLAUDE_SKILL_DIR}/../../standards/<name>.md`, which the negative
168
+ * lookbehind rejects, so a citation already rewritten to the resolving form
169
+ * does not fail this check a second time.
170
+ *
171
+ * Reported only where `isStandardsPathResolvable` confirms the match
172
+ * resolves against this checkout, the same gate `DOCS_PATH` runs. A body
173
+ * illustrating the shape a project's own `standards/<name>.md` takes writes
174
+ * the identical placeholder token this pattern matches, such as
175
+ * `standards/<slug>.md` in `create-standard/SKILL.md` or `standards/<name>.md`
176
+ * in `migration-standards-drop/SKILL.md`, and neither resolves to a real
177
+ * file. Four such placeholders surfaced across three files the first time
178
+ * this pattern ran unresolved, measured 2026-09-06.
179
+ */
180
+ const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
181
+
151
182
  /**
152
183
  * A phase-label-shaped token: exactly two numeric groups, with a negative
153
184
  * lookahead rejecting a third.
@@ -173,7 +204,12 @@ export interface ShippedReference {
173
204
  readonly file: string
174
205
  /** One-based, matching the `file:line` form a reader clicks. */
175
206
  readonly line: number
176
- readonly kind: 'pull-request' | 'commit' | 'docs-path' | 'phase-label'
207
+ readonly kind:
208
+ | 'pull-request'
209
+ | 'commit'
210
+ | 'docs-path'
211
+ | 'standards-path'
212
+ | 'phase-label'
177
213
  /** The reference as written, so a report names the token to qualify. */
178
214
  readonly text: string
179
215
  /**
@@ -202,18 +238,37 @@ function isDocsPathResolvable(
202
238
  return existsSync(join(root, path))
203
239
  }
204
240
 
241
+ /**
242
+ * Whether `file` sits in the one corpus `STANDARDS_PATH` gates.
243
+ *
244
+ * `REQUIREMENT.md` is excluded for the reason `598-authoring-layout.md`
245
+ * leaves it alone: a maintainer or an audit command reads that file rather
246
+ * than a session loading it, so the resolver rule this pattern enforces
247
+ * never applies there.
248
+ */
249
+ function isStandardsPathScope(file: string): boolean {
250
+ return file.startsWith('claude/skills/') && !file.endsWith('/REQUIREMENT.md')
251
+ }
252
+
253
+ /** Whether a `STANDARDS_PATH` match names a real file in this checkout. */
254
+ function isStandardsPathResolvable(path: string, root: string): boolean {
255
+ return existsSync(join(root, path))
256
+ }
257
+
205
258
  /**
206
259
  * Every reference in one shipped file that no marker mutes.
207
260
  *
208
261
  * The corpus walk is deliberately absent, which lets most of the shape be
209
262
  * tested against a string rather than against a fixture. That is the seam
210
263
  * `headingCitationsIn` draws in `src/claude/skills-headings.ts` and `citationsIn`
211
- * draws in `skills-reach.ts`. `DOCS_PATH` is the one pattern that still needs
212
- * a filesystem, since resolving against this checkout is the only thing that
213
- * separates its two readings, so it takes `root` as the one caller-supplied
214
- * exception to that rule. `root` is required rather than defaulted, since a
215
- * caller that dropped it silently would report zero docs-path findings
216
- * rather than raising, which is the wrong failure direction for a gate.
264
+ * draws in `skills-reach.ts`. `DOCS_PATH` and `STANDARDS_PATH` are the two
265
+ * patterns that still need a filesystem, since resolving against this
266
+ * checkout is the only thing that separates a real citation from an
267
+ * illustration for either, so `referencesIn` takes `root` as the one
268
+ * caller-supplied exception to that rule. `root` is required rather than
269
+ * defaulted, since a caller that dropped it silently would report zero
270
+ * findings for both rather than raising, which is the wrong failure
271
+ * direction for a gate.
217
272
  *
218
273
  * The unit is the match rather than the line, unlike those two, because one
219
274
  * line here can carry three separate tokens each needing its own repair and a
@@ -272,6 +327,18 @@ export function referencesIn(
272
327
  })
273
328
  }
274
329
 
330
+ if (isStandardsPathScope(file)) {
331
+ for (const match of line.matchAll(STANDARDS_PATH)) {
332
+ if (!isStandardsPathResolvable(match[0], root)) continue
333
+ references.push({
334
+ file,
335
+ line: index + 1,
336
+ kind: 'standards-path',
337
+ text: match[0],
338
+ })
339
+ }
340
+ }
341
+
275
342
  for (const match of line.matchAll(PHASE_LABEL)) {
276
343
  references.push({
277
344
  file,
@@ -26,7 +26,7 @@ A wireframe works when someone can rebuild the surface from it without opening t
26
26
 
27
27
  - What is on screen, and where does it sit relative to everything else?
28
28
  - Which states can a visitor reach, and what does each one look like?
29
- - What does it say, word for word?
29
+ - What does its structural copy say, word for word, and where does its long-form content come from?
30
30
 
31
31
  A wireframe that fails these is non-conforming regardless of whether it satisfies every section rule below. The fences are the means. These three questions are the test.
32
32
 
@@ -60,7 +60,8 @@ Both fields feed `.claude/wireframes/index.md` when regenerated.
60
60
 
61
61
  ## Copy
62
62
 
63
- - Carry UI copy verbatim in the ASCII block or a short list below it. The wireframe is the source of truth for on-screen text.
63
+ - Carry short structural text verbatim, in the ASCII block or a short list below it: labels, headings, empty-state strings, and nav or footer copy. The wireframe is the source of truth for this text.
64
+ - Cite the source file for long-form or article-body content instead of duplicating it. A lede sentence or a body paragraph pulled into an ASCII figure drifts the moment its source changes, and nothing checks a fenced block against prose elsewhere. A citation elsewhere in the file does not excuse the block itself from still carrying the duplicate. Trim the block once the citation exists.
64
65
  - Mark copy that is dynamic or templated so a reader does not treat a placeholder as final text.
65
66
 
66
67
  ## Behavior
@@ -113,6 +114,7 @@ description: <when and where the surface appears>
113
114
 
114
115
  - <on-screen text, word for word>
115
116
  - <text the surface templates>: <marked so a reader does not read it as final>
117
+ - <long-form content>: cited at <its source file>, not duplicated here
116
118
 
117
119
  ## Behavior
118
120
 
@@ -1,7 +1,33 @@
1
+ const SUBJECT_STARTS_WITH_LETTER = /^[A-Za-z]/
2
+ const SUBJECT_LEADING_LETTERS = /^[A-Za-z]+/
3
+
4
+ // Checks the first word alone, unlike the built-in subject-case rule, which
5
+ // tests the whole subject and would reject a legitimate capitalized proper
6
+ // noun anywhere past the first word.
7
+ const subjectFirstWordCase = (parsed) => {
8
+ const { subject } = parsed
9
+
10
+ if (
11
+ typeof subject !== 'string' ||
12
+ !SUBJECT_STARTS_WITH_LETTER.test(subject)
13
+ ) {
14
+ return [true]
15
+ }
16
+
17
+ const leadingWord = SUBJECT_LEADING_LETTERS.exec(subject)[0]
18
+
19
+ return [
20
+ leadingWord === leadingWord.toLowerCase(),
21
+ 'subject must start with a lowercase word',
22
+ ]
23
+ }
24
+
1
25
  const config = {
2
26
  extends: ['@commitlint/config-conventional'],
27
+ plugins: [{ rules: { 'subject-first-word-case': subjectFirstWordCase } }],
3
28
  rules: {
4
29
  'subject-case': [0],
30
+ 'subject-first-word-case': [2, 'always'],
5
31
  'header-max-length': [2, 'always', 72],
6
32
  'scope-case': [2, 'always', 'lower-case'],
7
33
  'subject-full-stop': [2, 'never', '.'],
@@ -11,7 +11,8 @@ fi
11
11
  bun run build
12
12
  bun run preview >/dev/null 2>&1 &
13
13
  PREVIEW_PID=$!
14
- trap 'kill "$PREVIEW_PID" 2>/dev/null || true; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
14
+ # shellcheck disable=SC2154 # pid is bound by the for loop inside the single-quoted trap body, which shellcheck does not track there
15
+ trap 'kill "$PREVIEW_PID" 2>/dev/null || true; if command -v lsof >/dev/null 2>&1; then for pid in $(lsof -ti tcp:"$PREVIEW_PORT" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done; else echo "lsof not found on PATH; cannot confirm the preview port is clear of a detached grandchild." >&2; fi; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
15
16
 
16
17
  for _ in $(seq 1 40); do
17
18
  if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then