@erclx/canon 4.24.0 → 4.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-feature/SKILL.md +10 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +63 -0
- package/claude/skills/draft-and-pick/SKILL.md +92 -0
- package/docs/agents/capture.md +7 -1
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/tasks.md +14 -10
- package/docs/workflow/ai-workflow.md +1 -0
- package/docs/workflow/visual-design-workflow.md +1 -0
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +93 -0
- package/src/claude/cases/misc.ts +5 -0
- package/src/pr/paths.ts +10 -7
- package/src/tasks/validate.ts +71 -9
- package/standards/tasks.md +8 -4
package/README.md
CHANGED
|
@@ -57,6 +57,18 @@ Each domain has a canonical source in this repo and a thin install or sync CLI o
|
|
|
57
57
|
- Transcripts: `canon transcripts` pulls a YouTube transcript with metadata frontmatter into any repo
|
|
58
58
|
- Sandbox: scenario-based scaffolds that provision an isolated project state for verifying each domain flow
|
|
59
59
|
|
|
60
|
+
Those domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A standard stays here and is opened by name, so there is no copy in your repo to drift from this one.
|
|
65
|
+
|
|
66
|
+
Governance is the third shape, and it is worth seeing rather than reading about, because the glob beside each rule is what decides whether it reaches a session at all.
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
Stacks compose, so a react project inherits node and node inherits base. A rule with a glob loads only when a matching path is edited, and a rule with none loads every session. Both counts and every row above are read from the catalogs when the image is built.
|
|
71
|
+
|
|
60
72
|
## Documentation
|
|
61
73
|
|
|
62
74
|
Scaffolding your first project? Start with target projects, then the AI workflow loop. Everything else answers questions that arrive later.
|
|
@@ -48,6 +48,16 @@ What this skill adds on top of the standard:
|
|
|
48
48
|
- Prefer `None identified.` over low-signal fillers. A small feature should produce a short plan, not a padded one.
|
|
49
49
|
- When three or more questions remain, keep chat output to the file pointer plus a short summary. Inline chat is fine when two or fewer remain.
|
|
50
50
|
|
|
51
|
+
## Pull request boundaries
|
|
52
|
+
|
|
53
|
+
A plan sequencing its work into batches declares where one pull request ends, here rather than at ship time. A dependency chain cannot be split once it is built, so the choice exists only while the batches are still a plan.
|
|
54
|
+
|
|
55
|
+
- One batch is one pull request. Carry one entry per batch naming what it targets, so a plan opening five says five before the first line is written.
|
|
56
|
+
- A dependent batch stacks rather than merging into its neighbor. The batches are built in order and depend on each other in that order, which is what a stack expresses, so the dependency argues for stacking and never for collapsing two batches into one review.
|
|
57
|
+
- Mark each batch dependent or independent by comparing the file sets rather than the descriptions. A batch sharing no file with another is independent and earns its own branch even where both arrived in one request. One run measured at 68 files carried three such batches into a single review because nothing drew the boundary.
|
|
58
|
+
- Sequence a sweep last. A batch deliberately rewriting files earlier batches touched is coherent as the final one and forces every batch behind it into one review anywhere else.
|
|
59
|
+
- Keep each batch's commits contiguous once the work starts. A later fix to an earlier batch belongs on that batch's own commits, since a batch interrupted by another cannot be lifted onto its own branch afterward.
|
|
60
|
+
|
|
51
61
|
## Step 4: output
|
|
52
62
|
|
|
53
63
|
Decide the mode based on what Step 3 produced:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: draft-and-pick
|
|
3
|
+
description: Why a decision settled by looking needs its own surface, and where its boundary against the planning, audit, measurement, and recording skills falls
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Draft and pick requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a session facing a decision nobody can settle from a diff:
|
|
11
|
+
|
|
12
|
+
- Describes two options in prose and asks the operator to choose. The operator decides by looking, so a described option is not an option and the answer that comes back is arbitrary.
|
|
13
|
+
- Produces one answer for a decision that was taste, so the operator never sees what they were choosing between and the call is taken silently by whoever wrote the code.
|
|
14
|
+
- Reports that a candidate reads well without rendering it, so the claim rests on the markup having been written rather than on the page having been seen.
|
|
15
|
+
- Hands over one image per arm, which asks the operator to hold the differences in memory rather than showing them. The comparison is the artifact and a set of separate files is not one.
|
|
16
|
+
- Varies three properties at once across the arms, so the pick cannot say which difference decided it and the next iteration guesses.
|
|
17
|
+
- Writes the candidates into the real surface, so the losing arms have to be unpicked out of a tree the operator has not judged yet, and a variant left behind a flag becomes a second design nobody maintains.
|
|
18
|
+
- Stops the loop when the arms stop differing rather than when the operator says the pick is right, which ends the run on the session's own patience.
|
|
19
|
+
- Reads a driver record as a verdict and drops an arm on it. `canon drive` reports findings and never gates, so a run filtering the arms on a probe reading has made a claim the probe catalog has not earned.
|
|
20
|
+
- Reads a color without compositing its alpha, samples a round control at the corner of its bounding box, or confirms a panel's dimensions while it sits above the viewport. Each reads as a passing measurement about something no reader sees.
|
|
21
|
+
- Meets a machine with no browser binary and reports on the arms anyway, since nothing in the default flow separates a render that failed from one that was never attempted.
|
|
22
|
+
|
|
23
|
+
## Must
|
|
24
|
+
|
|
25
|
+
- Produce three to five arms with the shipping state among them, each carrying an id, a label, and what the arm costs.
|
|
26
|
+
- Author the whole candidate set as one self-contained page and render it once, so the comparison arrives as one image.
|
|
27
|
+
- Vary one property across the arms, so the answer names a difference.
|
|
28
|
+
- Render before reporting on any arm, and report a missing browser binary as a refusal naming the repair rather than describing an arm nobody has seen.
|
|
29
|
+
- Take the pick through the structured question surface, with the recommendation ranked first and marked, and every option carrying its cost.
|
|
30
|
+
- Keep every write inside the run's scratch folder until the pick is final.
|
|
31
|
+
- Apply the winning arm and delete the losing ones in the same change.
|
|
32
|
+
- Serve the candidates live where the decision is one the operator has to drive, since a still cannot answer how a gesture feels or how a pace reads.
|
|
33
|
+
|
|
34
|
+
## Must not
|
|
35
|
+
|
|
36
|
+
- Take the pick where two arms are both defensible and the difference is taste. That call is the operator's, and taking it silently is the failure this skill exists to prevent.
|
|
37
|
+
- Draft candidates for a decision the operator has not asked to make. Each arm costs a render, and a run offering options everywhere spends their attention rather than saving it.
|
|
38
|
+
- Restate the mechanics of the render, the address, or the probes. `canon capture`, `canon serve`, and `canon drive` own those and their help text is the source, so a second copy here hands one case two answers.
|
|
39
|
+
- Read a `canon drive` finding as a gate over the arms.
|
|
40
|
+
- Fire on a request that already names one answer.
|
|
41
|
+
|
|
42
|
+
## Guards
|
|
43
|
+
|
|
44
|
+
The refusal strings sit in the body, since the runtime loads that file and ignores this one. Two conditions stop a run: a request naming one answer and asking for it to be built, and a decision with no visible form for a render to show.
|
|
45
|
+
|
|
46
|
+
## Out of scope
|
|
47
|
+
|
|
48
|
+
- `claude-feature` plans the work once the answer is settled, and declares the pull request boundary its plan carries. This produces the answer to pick from and stops before the plan.
|
|
49
|
+
- `claude-ux-audit` reads source to find roughness and reports it. This takes its input from the operator and changes nothing until they pick.
|
|
50
|
+
- `claude-ux-measure` measures what a running interface costs to paint. This measures whatever a visual claim depends on, which is usually geometry or contrast rather than cost.
|
|
51
|
+
- `claude-ui-test` writes tests for a change already made. This runs before there is a change to test.
|
|
52
|
+
- `canon-screencast` scripts a recording of something already built. This has nothing built yet.
|
|
53
|
+
- `canon capture`, `canon serve`, and `canon drive` own the render, the address, and the probes, and are invoked rather than reimplemented.
|
|
54
|
+
|
|
55
|
+
### What did not travel
|
|
56
|
+
|
|
57
|
+
This descends from a project-local skill whose interactive half was written against one repository. Three of its capabilities do not travel, and the three verbs above are what stands in their place:
|
|
58
|
+
|
|
59
|
+
- An arm switcher compiled into the project's own page, driven by a query parameter. `canon serve` over a self-contained scratch page replaces it, which puts the arms in scratch rather than in the source and removes the call site a run had to remember to delete.
|
|
60
|
+
- A copy cycle keyed to canonical page text held in a second repository. No toolkit surface has that shape, so a project holding one owns the second edit and this skill states nothing about it.
|
|
61
|
+
- A walker that reads every control on every page and groups them by treatment. `canon drive` probes one page and reports findings, so a sweep across every surface of a site is out of reach here and a run needing one measures by hand.
|
|
62
|
+
|
|
63
|
+
The capture mechanics traveled with a change of owner rather than being lost. They sat in a project-local path-scoped rule that fired whether or not the skill ran, and `canon capture` owns them here, which is why this file bars restating them rather than naming a rule to defer to.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: draft-and-pick
|
|
3
|
+
description: Drafts several candidates for a decision judged by looking, renders them side by side on one page, hands the operator the addresses, takes the pick through the structured question surface, and loops on the pick until they stop. Use when asked to "draft some options", "show me a few versions", "try a few variations", "mock up alternatives", "give me candidates for X", or when a choice is taste rather than correctness. Do NOT use when the request already names the answer and asks for it to be built, which is `claude-feature`. Do NOT use to read source for roughness, which is `claude-ux-audit`, to measure what a running interface costs to paint, which is `claude-ux-measure`, to write tests for a change already made, which is `claude-ui-test`, or to script a recording, which is `canon-screencast`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Draft and pick
|
|
7
|
+
|
|
8
|
+
Some decisions are settled by looking rather than by reasoning, and no draft is wrong until one is picked. Every step here puts several candidates in front of the operator and keeps the real surface untouched until they have chosen.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If the request names one answer and asks for it to be built, stop: `❌ This names one answer, so there is nothing to pick between. Use /canon:claude-feature.`
|
|
13
|
+
- If the decision has no visible form, stop: `❌ Nothing to look at. Drafting candidates needs a decision a render can show.`
|
|
14
|
+
- Draft no candidate for a decision the operator has not asked to make. A run offering options everywhere spends their attention rather than saving it.
|
|
15
|
+
|
|
16
|
+
## Step 1: name the decision and the arms
|
|
17
|
+
|
|
18
|
+
1. State the decision in one sentence, naming what changes between arms and what stays fixed.
|
|
19
|
+
2. Derive a kebab slug from that sentence. Every file this run writes goes to `.canon/tmp/<slug>/`, per `.claude/rules/core/055-scratch.md`.
|
|
20
|
+
3. Write one arm per candidate, each carrying an id, a label, and what the arm costs. An arm with no stated cost is not an option.
|
|
21
|
+
4. Make the current state arm `0`, so the baseline is a candidate rather than an absence. A decision with nothing shipped yet says so and starts at arm `1`.
|
|
22
|
+
5. Stop at three to five arms. Two is a comparison the operator can hold in prose, and past five the pick stops being a look and becomes a sort.
|
|
23
|
+
|
|
24
|
+
## Step 2: author the candidate set as one page
|
|
25
|
+
|
|
26
|
+
Write every arm side by side on one self-contained HTML page at `.canon/tmp/<slug>/candidates.html`.
|
|
27
|
+
|
|
28
|
+
- One page, never one file per arm. The comparison is the artifact, and several images handed over separately ask the operator to hold the differences in memory.
|
|
29
|
+
- Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
|
|
30
|
+
- Inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
|
|
31
|
+
- Declare a font stack the machine resolves, such as `system-ui` behind a generic fallback. The render refuses a page that would rewrap against a substitute rather than shipping a false comparison, so a page naming no font at all is refused on whatever the default resolves to.
|
|
32
|
+
- Vary one property across the arms. A page whose arms differ in three ways answers no question, since the pick cannot say which difference decided it.
|
|
33
|
+
|
|
34
|
+
## Step 3: render and hand off
|
|
35
|
+
|
|
36
|
+
Render the page, then look at what came back:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
canon capture .canon/tmp/<slug>/candidates.html --selector <element>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `--selector` has no default and the command refuses without it. Name the element wrapping the arms rather than `body`, which crops to whatever the page's own margins leave.
|
|
43
|
+
- `canon capture` and `canon drive` both need a browser binary the toolkit does not install. When either refuses for that reason, report the refusal and name `bunx playwright install chromium` as the repair, then stop rather than describing an arm nobody has seen.
|
|
44
|
+
- Serve the page instead of capturing it when the operator has to drive the decision, such as a hover response, a scroll-linked position, or a pace. Start `canon serve .canon/tmp/<slug> --entry candidates.html` in the background and read the link off its record, since the printed link opens `index.html` without that flag and the scratch folder holds no such file. A still answers how a thing looks and answers none of those.
|
|
45
|
+
- Hand over the address rather than a description. Emit the PNG path on its own line, and the link beside it where the page is served.
|
|
46
|
+
- Never report a visual result you have not looked at. A claim about appearance with no render behind it is a guess.
|
|
47
|
+
|
|
48
|
+
## Step 4: take the pick
|
|
49
|
+
|
|
50
|
+
Put the choice to the operator through the structured question surface, per `.claude/rules/core/005-behavior.md`.
|
|
51
|
+
|
|
52
|
+
- One option per arm, labeled with the arm's id and carrying its cost as the description.
|
|
53
|
+
- Rank the recommendation first and mark it `(Recommended)`.
|
|
54
|
+
- Author the real arms only. The surface appends its own escapes for a free-text answer and for reopening the question, so writing either as an option ships a duplicate the tool rejects.
|
|
55
|
+
- Take no pick on the operator's behalf when two arms are both defensible and the difference is taste. That call is theirs, and a silent one is the failure this skill exists to prevent.
|
|
56
|
+
|
|
57
|
+
## Step 5: loop on the pick
|
|
58
|
+
|
|
59
|
+
1. Narrow the page to the picked arm, plus whatever the operator asked to change about it.
|
|
60
|
+
2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not.
|
|
61
|
+
3. Re-render, hand off again, and take the next answer.
|
|
62
|
+
4. Repeat until the operator says it is right. The loop stops on their word and on nothing else, so a run stopping because the arms stopped differing has stopped early.
|
|
63
|
+
5. Hold the real surface untouched across every iteration. Nothing outside `.canon/tmp/<slug>/` changes until the pick is final.
|
|
64
|
+
|
|
65
|
+
## Step 6: close
|
|
66
|
+
|
|
67
|
+
1. Apply the winning arm to the real surface, in one change.
|
|
68
|
+
2. Close out whatever document stated the decision as open, in the same change, naming the arm that won and the ones that stayed defensible. A pick that changes a surface and records nothing about why leaves the next reader to re-derive it from a diff, and the losing arms are gone by the next step. Skip this where nothing stated the decision.
|
|
69
|
+
3. Delete `.canon/tmp/<slug>/` and every losing arm with it. A variant left behind is a second design nobody maintains.
|
|
70
|
+
4. Report the scratch folder as still standing when that delete is refused, naming the path for the operator to remove, rather than closing on a report the tree contradicts. The pick is applied either way, so the run has done its work and the folder is what outlives it.
|
|
71
|
+
5. Report every surface that changed, each on its own line, and name the arm that won by its id and its cost.
|
|
72
|
+
|
|
73
|
+
## Reading a measurement
|
|
74
|
+
|
|
75
|
+
A capture proves appearance and a measurement proves a relationship, so reach for the second whenever the claim is about a number, such as a contrast ratio, a column width, or a tap target. `canon drive` runs the probes and ships the failure modes each one carries.
|
|
76
|
+
|
|
77
|
+
Three rules no probe reaches:
|
|
78
|
+
|
|
79
|
+
- Composite alpha before reading a color. A `color-mix` toward transparent resolves to channels plus an alpha, and reading those channels as opaque reports a color nobody sees.
|
|
80
|
+
- Sample inside the shape. A patch taken at the corner of a bounding box misses a round control and reads the page behind it, which is how a ground repair measured as no change at all.
|
|
81
|
+
- Ask whether a reader would see the thing, not only whether it has the right shape. A panel reported a healthy 1517 by 639 for as long as it sat 1868px above the viewport, and every check that read its size passed.
|
|
82
|
+
|
|
83
|
+
`canon drive` reports findings and never gates, by its own help text. Read its record as evidence handed to the operator rather than as a filter over the arms, since a run dropping an arm on a probe reading has made a claim the probe catalog has not earned.
|
|
84
|
+
|
|
85
|
+
## What this delegates
|
|
86
|
+
|
|
87
|
+
Cite these rather than restating them. A step reimplemented here rots against the skill that owns it.
|
|
88
|
+
|
|
89
|
+
- `claude-feature` plans the work once the pick is made, and declares the pull request boundary that plan carries
|
|
90
|
+
- `write-human` carries the voice for any copy an arm puts in front of a reader
|
|
91
|
+
- `git-stage`, `git-pr`, and `git-followup` carry the commits and the pull request
|
|
92
|
+
- `claude-review` and `claude-address-review` run the review pass
|
package/docs/agents/capture.md
CHANGED
|
@@ -15,7 +15,13 @@ canon capture assets --selector .window --out .canon/review/captures
|
|
|
15
15
|
|
|
16
16
|
`--selector` is required and every example above passes it. The element a capture crops to belongs to the page, not to the command, so there is no value that could be right for an arbitrary project's markup. `.window` is what this repository's own two sources declare, and a project renders its own pages by naming whatever theirs declare.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## What this repository captures
|
|
19
|
+
|
|
20
|
+
`assets/` here holds four sources, so one run over the folder rebuilds every one. None is edited by hand. `scripts/core/regen-hero.sh` writes each from a template beside it, filling one shared value map into all of them, and `bun run check` regenerates them and fails on the difference.
|
|
21
|
+
|
|
22
|
+
Three of the four take catalog data, so a stack gaining a rule moves the frame on the next run. The terminal text on `install.html` is the exception, coming from a real run and held in the template rather than in the generated file.
|
|
23
|
+
|
|
24
|
+
The folder is read flat and never descends, by the regeneration script, by this command, and by the drift stage alike. A source in a subfolder is skipped by all three with nothing reported, so a new frame takes a name prefix rather than a folder of its own.
|
|
19
25
|
|
|
20
26
|
Only the HTML is asserted for drift. The PNG is a chromium render whose bytes move with the browser version, so rebuild it with `canon capture assets --selector .window` when the check reports the HTML changed.
|
|
21
27
|
|
|
@@ -58,7 +58,7 @@ A span anywhere in the bullet has to survive all of these:
|
|
|
58
58
|
| No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^` | A backticked command, a placeholder, a glob, a pattern |
|
|
59
59
|
| No `://` and no leading `/`, `~`, `@`, `#`, `!` | A URL, an absolute path, a module alias |
|
|
60
60
|
| Contains `/` | A bare filename with no folder around it |
|
|
61
|
-
|
|
|
61
|
+
| Not a dotted-decimal segment, or ends with `/` | A dotted number such as an address |
|
|
62
62
|
| Not a single top-level folder | `src/`, which nobody claims to have rewritten whole |
|
|
63
63
|
| A `file:line` span leads its bullet | A citation into a file the bullet is describing |
|
|
64
64
|
| The region carries no no-change marker | A bullet recording a file it deliberately left alone |
|
package/docs/agents/tasks.md
CHANGED
|
@@ -160,15 +160,15 @@ canon tasks validate --json
|
|
|
160
160
|
|
|
161
161
|
Seven checks run. Plan and Collisions reach one half each of the `## Run now` test the board standard states. Mapping and Grouping test the folder contract and hold for every group, and Shape holds for every group too, ahead of the four. Ordering reaches only the `## Needs a plan` rows, and Blockers reaches every row outside `## Run now`:
|
|
162
162
|
|
|
163
|
-
| Check | What it reports
|
|
164
|
-
| ---------- |
|
|
165
|
-
| Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed
|
|
166
|
-
| Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line
|
|
167
|
-
| Mapping | A row or backlog line naming no task file, and a task file neither surface names
|
|
168
|
-
| Grouping | A task carrying a row in more than one readiness group, or on both surfaces
|
|
169
|
-
| Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits
|
|
170
|
-
| Collisions | Two `## Run now` rows whose Touches columns name a path in common
|
|
171
|
-
| Blockers | A parked row whose blocker has stopped holding, or whose cited task resolves nowhere
|
|
163
|
+
| Check | What it reports |
|
|
164
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
|
|
166
|
+
| Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line |
|
|
167
|
+
| Mapping | A row or backlog line naming no task file, and a task file neither surface names |
|
|
168
|
+
| Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
|
|
169
|
+
| Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
|
|
170
|
+
| Collisions | Two `## Run now` rows whose Touches columns name a path in common |
|
|
171
|
+
| Blockers | A parked row whose blocker has stopped holding, or whose cited task resolves nowhere |
|
|
172
172
|
|
|
173
173
|
Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
|
|
174
174
|
|
|
@@ -178,7 +178,11 @@ Mapping spans two surfaces, because a task sits on `priority.md` when it would p
|
|
|
178
178
|
|
|
179
179
|
A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name is still reported as reaching neither surface. A project carrying no `backlog.md` reads as an empty backlog rather than a refusal, which leaves the one-to-one mapping this check ran before the second surface existed.
|
|
180
180
|
|
|
181
|
-
Ordering reads
|
|
181
|
+
Ordering reads a `## Needs a plan` row's `Waiting on` cell for the position it claims, and reports two failures off one walk. A row stating an ordinal is checked against where it actually sits, which is `row-misordered`. A row claiming no position in either form it may take is `row-unranked`, since its cell argues the task matters and ranks it against nothing, which leaves the order recording when each row was filed.
|
|
182
|
+
|
|
183
|
+
The ordinal phrase is prose rather than data, searched for anywhere in the cell rather than at its start, since every live row states its position at the end of a sentence rather than at the front. The vocabulary stops at `first` through `twentieth` plus `last`, since a parser strict enough to catch a gap would otherwise flag a row phrased correctly and differently, and bounding it to those words is what keeps a cell reading `Untestable from here` from matching on `from`. One comparison against the real position catches a gap, a duplicate, and a sequence starting somewhere other than first alike.
|
|
184
|
+
|
|
185
|
+
The comparative phrase is bounded the same way and for the same reason. A closed verb list of `leads`, `heads`, `opens`, `closes`, `trails`, `precedes`, `follows`, `outranks`, and `sits under`, `above`, or `below` has to sit in one clause with a positional object, which is a `vNN.N` phase label, the word `group`, or `row` or `rows`. Both halves are needed, since a cell reading `it closes a gap the reference gate leaves open` carries the verb and claims no position, and the clause bound is what stops a verb in one half of the cell pairing with an object in the other. An ordinal exempts the row, being a comparative claim already. The residue runs both ways: a cell phrased comparatively and unusually reports as unranked, which is the false negative and the safe direction for a check over prose, and a vocabulary verb reaching a positional object non-positionally reports as ranked, which is the false positive. Separating the second from a real position claim means grading prose, so the bound stays where it is.
|
|
182
186
|
|
|
183
187
|
The collision check is the one a person cannot run by eye. Paths come from the backticked spans in the Touches column, a span naming no file is dropped, and a directory collides with any file beneath it. A `## Run now` row whose column parses to nothing is reported rather than skipped, since a row stating no file set makes a claim nothing can check.
|
|
184
188
|
|
|
@@ -212,6 +212,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
212
212
|
| `canon:claude-intake-answer` | When an intake folder holds unread slots waiting on your decision |
|
|
213
213
|
| `canon:claude-groundwork` | When the state is unmeasured and more than one approach is live |
|
|
214
214
|
| `canon:decision-escalate` | When open decisions turn on your preference and want batching into one set |
|
|
215
|
+
| `canon:draft-and-pick` | When the call is taste and wants several candidates rendered side by side |
|
|
215
216
|
| `canon:claude-tasks` | When a decided item needs a file on the board, or a shipped one needs archiving |
|
|
216
217
|
| `canon:claude-feature` | When the approach is settled and the next step is a plan |
|
|
217
218
|
|
|
@@ -39,6 +39,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
39
39
|
- `canon:claude-ui-test` for e2e test generation after UI changes
|
|
40
40
|
- `canon:claude-ux-audit` for UX gap detection on existing surfaces
|
|
41
41
|
- `canon:claude-ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
42
|
+
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
42
43
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
43
44
|
|
|
44
45
|
### When to pick
|
package/package.json
CHANGED
|
@@ -226,8 +226,101 @@ for (const [label, list] of [
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// A terminal frame renders lines into a `<pre>` where the hero renders names
|
|
230
|
+
// into divs, so the two cannot share a value however similar the catalog read
|
|
231
|
+
// behind them looks. Padding happens before escaping, since an entity is longer
|
|
232
|
+
// than the character it replaces and would push a column out of line.
|
|
233
|
+
const pad = (value, width) => value + " ".repeat(Math.max(0, width - value.length))
|
|
234
|
+
const frameRow = (cells) => `<span class="frame">│</span> <span class="ok">✓</span> ${cells}`
|
|
235
|
+
|
|
236
|
+
// The frame sets `white-space: pre` at a fixed window width, so an overlong
|
|
237
|
+
// cell is clipped by the window edge rather than wrapped. Truncating here is
|
|
238
|
+
// what keeps the longest row inside the capture, and the ellipsis is what stops
|
|
239
|
+
// a clipped value from reading as the whole value.
|
|
240
|
+
const GLOB_WIDTH = 44
|
|
241
|
+
const clip = (value, width) =>
|
|
242
|
+
value.length <= width ? value : `${value.slice(0, width - 1)}…`
|
|
243
|
+
|
|
244
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`
|
|
245
|
+
|
|
246
|
+
const govStackRows = gov.stacks
|
|
247
|
+
.map((stack) =>
|
|
248
|
+
frameRow(
|
|
249
|
+
`<span class="name">${escape(pad(stack.name, 17))}</span>` +
|
|
250
|
+
`<span class="muted">${escape(pad(plural(stack.rules.length, "rule"), 11))}</span>` +
|
|
251
|
+
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
.join("\n")
|
|
255
|
+
|
|
256
|
+
// Sampled from the stack-reached subset for the reason the hero column is, and
|
|
257
|
+
// showing each rule beside the glob that loads it rather than its description,
|
|
258
|
+
// since the glob is the mechanism a reader cannot otherwise see.
|
|
259
|
+
const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
|
|
260
|
+
const govRuleRows = sample(deliveredEntries)
|
|
261
|
+
.map((entry) => {
|
|
262
|
+
const globs = [entry.paths ?? []].flat()
|
|
263
|
+
// A rule carrying no glob is not unscoped, it loads every session, and
|
|
264
|
+
// saying so is the contrast that makes the column mean anything.
|
|
265
|
+
const scope = globs.length > 0 ? clip(globs.join(" "), GLOB_WIDTH) : "every session"
|
|
266
|
+
return frameRow(
|
|
267
|
+
`<span class="name">${escape(pad(slug(entry), 24))}</span>` +
|
|
268
|
+
`<span class="domain">${escape(pad(`[${entry.domain}]`, 12))}</span>` +
|
|
269
|
+
`<span class="muted">${escape(scope)}</span>`,
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
.join("\n")
|
|
273
|
+
|
|
274
|
+
// A standard either governs an artifact by path or is opened by name when a
|
|
275
|
+
// session decides it needs it. Rendering the first and naming the second is the
|
|
276
|
+
// contrast, since nothing else in the catalog tells a reader that the corpus
|
|
277
|
+
// installs into no project and is read through a verb instead.
|
|
278
|
+
const standardEntries = JSON.parse(STANDARDS_JSON).standards
|
|
279
|
+
const standardRows = sample(standardEntries)
|
|
280
|
+
.map((entry) => {
|
|
281
|
+
const applies = [entry.appliesTo ?? []].flat()
|
|
282
|
+
const governs =
|
|
283
|
+
applies.length > 0 ? clip(applies.join(" "), GLOB_WIDTH) : "read by name"
|
|
284
|
+
return frameRow(
|
|
285
|
+
`<span class="name">${escape(pad(entry.name, 18))}</span>` +
|
|
286
|
+
`<span class="muted">${escape(governs)}</span>`,
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
.join("\n")
|
|
290
|
+
|
|
291
|
+
// A tooling stack is counted rather than listed, because what it lays down is
|
|
292
|
+
// dev dependencies, run scripts, and ignore groups rather than named entries a
|
|
293
|
+
// reader would recognize. The inheritance is the part worth showing, since it
|
|
294
|
+
// is why a stack carrying two of its own arrives with far more than two.
|
|
295
|
+
const toolingStackRows = toolingStacks
|
|
296
|
+
.map((stack) =>
|
|
297
|
+
frameRow(
|
|
298
|
+
`<span class="name">${escape(pad(stack.name, 14))}</span>` +
|
|
299
|
+
`<span class="muted">${escape(pad(plural(stack.devDeps, "dep"), 10))}</span>` +
|
|
300
|
+
`<span class="muted">${escape(pad(plural(stack.scripts, "script"), 12))}</span>` +
|
|
301
|
+
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
.join("\n")
|
|
305
|
+
|
|
306
|
+
for (const [label, rows] of [
|
|
307
|
+
["governance stack rows", govStackRows],
|
|
308
|
+
["governance rule rows", govRuleRows],
|
|
309
|
+
["standard rows", standardRows],
|
|
310
|
+
["tooling stack rows", toolingStackRows],
|
|
311
|
+
]) {
|
|
312
|
+
if (rows === "") {
|
|
313
|
+
console.error(`regen-hero: the ${label} rendered empty, refusing to write a blank frame`)
|
|
314
|
+
process.exit(1)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
229
318
|
const values = {
|
|
230
319
|
TOKENS: tokenCss,
|
|
320
|
+
GOV_STACK_ROWS: govStackRows,
|
|
321
|
+
GOV_RULE_ROWS: govRuleRows,
|
|
322
|
+
STANDARD_ROWS: standardRows,
|
|
323
|
+
TOOLING_STACK_ROWS: toolingStackRows,
|
|
231
324
|
SKILL_COUNT: String(skills.length),
|
|
232
325
|
RULE_COUNT: String(rules.length),
|
|
233
326
|
STANDARD_COUNT: String(standards.length),
|
package/src/claude/cases/misc.ts
CHANGED
|
@@ -7,6 +7,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
7
7
|
'Bundle up every open call that only I can make and ask me all at once.',
|
|
8
8
|
expect: 'decision-escalate',
|
|
9
9
|
},
|
|
10
|
+
{
|
|
11
|
+
prompt:
|
|
12
|
+
'Show me a few different treatments for this callout so I can pick one by looking.',
|
|
13
|
+
expect: 'draft-and-pick',
|
|
14
|
+
},
|
|
10
15
|
{
|
|
11
16
|
prompt: 'Fire up the dev server the way this project documents it.',
|
|
12
17
|
expect: 'project-commands',
|
package/src/pr/paths.ts
CHANGED
|
@@ -94,15 +94,18 @@ function isNotRepositoryPath(span: string): boolean {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Whether the span's last segment
|
|
97
|
+
* Whether the span's last segment is a bare dotted-decimal number, such as
|
|
98
|
+
* an address (`127.0.0.1`) or an unprefixed version number (`1.2.3`).
|
|
98
99
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* `src/serve/127.0.0.1` in a report over a body that was correct
|
|
100
|
+
* This is the one exclusion `resolveSpan` still applies to a non-directory
|
|
101
|
+
* span. A bare dotted number reaching the comparison is the shape that put
|
|
102
|
+
* `src/serve/127.0.0.1` in a report over a body that was correct, and every
|
|
103
|
+
* other segment carrying no extension, such as `.husky/post-merge`, now
|
|
104
|
+
* resolves.
|
|
102
105
|
*/
|
|
103
|
-
function
|
|
106
|
+
function isDottedNumber(span: string): boolean {
|
|
104
107
|
const segment = span.slice(span.lastIndexOf('/') + 1)
|
|
105
|
-
return
|
|
108
|
+
return /^\d+(?:\.\d+)+$/.test(segment)
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
/** Blanks every backticked span so a cue search never fires inside one. */
|
|
@@ -285,7 +288,7 @@ function resolveSpan(span: string): ResolvedSpan | undefined {
|
|
|
285
288
|
: { path: span, directory: true }
|
|
286
289
|
}
|
|
287
290
|
|
|
288
|
-
return
|
|
291
|
+
return isDottedNumber(span) ? undefined : { path: span, directory: false }
|
|
289
292
|
}
|
|
290
293
|
|
|
291
294
|
/**
|
package/src/tasks/validate.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const FINDING_KINDS = [
|
|
|
43
43
|
'row-misshapen',
|
|
44
44
|
'row-untabled',
|
|
45
45
|
'row-misordered',
|
|
46
|
+
'row-unranked',
|
|
46
47
|
'touches-unstated',
|
|
47
48
|
'touches-collided',
|
|
48
49
|
'blocker-settled',
|
|
@@ -103,6 +104,8 @@ export interface BoardRow {
|
|
|
103
104
|
readonly waiting: string | undefined
|
|
104
105
|
/** The ordinal phrase a `Waiting on` cell states about its own position, undefined when the cell carries none. */
|
|
105
106
|
readonly ordinal: OrdinalWord | 'last' | undefined
|
|
107
|
+
/** Whether a `Waiting on` cell ranks its row against a sibling row or a class of rows, false when the group fixes no such column. */
|
|
108
|
+
readonly ranked: boolean
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
export interface ValidateReport {
|
|
@@ -277,6 +280,45 @@ function readOrdinal(cell: string): OrdinalWord | 'last' | undefined {
|
|
|
277
280
|
return LAST_AT_END.test(cell) ? 'last' : undefined
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
const RANK_VERB =
|
|
284
|
+
/\b(?:leads|heads|opens|closes|trails|precedes|follows|outranks|sits\s+(?:under|above|below))\b/i
|
|
285
|
+
|
|
286
|
+
/** A phase label, the group a row opens or closes, or the sibling rows it is ranked among. */
|
|
287
|
+
const RANK_OBJECT = /\bv\d+\.\d+\b|\bgroups?\b|\brows?\b/i
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A clause boundary, which is any of the three punctuation marks that end one
|
|
291
|
+
* plus the two conjunctions every live cell uses to hang its reason off its
|
|
292
|
+
* position claim. All three marks are bounded away from digits on both sides,
|
|
293
|
+
* which is what keeps a phase label one token, since `v80.4` is the commonest
|
|
294
|
+
* positional object on the board and splitting it at its own period would
|
|
295
|
+
* leave the verb holding nothing.
|
|
296
|
+
*/
|
|
297
|
+
const CLAUSE_BREAK = /(?<!\d)[.;,](?!\d)|\band\b|\bbecause\b/i
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Whether a `Waiting on` cell ranks its row against something rather than
|
|
301
|
+
* arguing that the row matters. The two are indistinguishable to a reader
|
|
302
|
+
* scanning the board, since a reason with no other row in it reads defensible
|
|
303
|
+
* on every row at once, which is how insertion order became the ordering.
|
|
304
|
+
*
|
|
305
|
+
* The test is a closed verb vocabulary sitting in one clause with a positional
|
|
306
|
+
* object, which mirrors `readOrdinal` bounding itself to a closed word list for
|
|
307
|
+
* the same reason: a parser loose enough to grade prose reports a correctly
|
|
308
|
+
* phrased row. Both halves are needed, since `closes` alone matches `it closes
|
|
309
|
+
* a gap the reference gate leaves open`, which claims no position at all. The
|
|
310
|
+
* clause bound is what keeps a verb in one half of the cell from pairing with
|
|
311
|
+
* an object in the other.
|
|
312
|
+
*
|
|
313
|
+
* A cell phrased comparatively and unusually reads as unranked, which is a
|
|
314
|
+
* false negative and the safe direction for a check over prose.
|
|
315
|
+
*/
|
|
316
|
+
function readRank(cell: string): boolean {
|
|
317
|
+
return cell
|
|
318
|
+
.split(CLAUSE_BREAK)
|
|
319
|
+
.some((clause) => RANK_VERB.test(clause) && RANK_OBJECT.test(clause))
|
|
320
|
+
}
|
|
321
|
+
|
|
280
322
|
function isRowLine(line: string): boolean {
|
|
281
323
|
return line.trimStart().startsWith('|')
|
|
282
324
|
}
|
|
@@ -388,6 +430,7 @@ export function readBoard(text: string): {
|
|
|
388
430
|
touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
|
|
389
431
|
waiting,
|
|
390
432
|
ordinal: waiting ? readOrdinal(waiting) : undefined,
|
|
433
|
+
ranked: waiting ? readRank(waiting) : false,
|
|
391
434
|
})
|
|
392
435
|
}
|
|
393
436
|
|
|
@@ -395,20 +438,39 @@ export function readBoard(text: string): {
|
|
|
395
438
|
}
|
|
396
439
|
|
|
397
440
|
/**
|
|
398
|
-
* Reports a `## Needs a plan` row
|
|
399
|
-
*
|
|
400
|
-
* its own would detect less, since row
|
|
401
|
-
* construction
|
|
402
|
-
* prose and the position have come apart.
|
|
441
|
+
* Reports the two ways a `## Needs a plan` row's stated position fails. A row
|
|
442
|
+
* carrying an ordinal is checked against where it actually sits, and reading
|
|
443
|
+
* the sequence for gaps and duplicates on its own would detect less, since row
|
|
444
|
+
* position is contiguous by construction and a hand-renumbered sequence is
|
|
445
|
+
* exactly a case where the prose and the position have come apart. A row
|
|
446
|
+
* carrying no position claim of either form is reported for that instead,
|
|
447
|
+
* since a cell arguing only that the task matters ranks it against nothing and
|
|
448
|
+
* leaves the position recording when the row was filed.
|
|
449
|
+
*
|
|
450
|
+
* Both are one question rather than two checks. An ordinal is already a
|
|
451
|
+
* comparative claim, so it exempts the row from the second half, and the two
|
|
452
|
+
* findings read off one walk over the same group.
|
|
403
453
|
*/
|
|
404
|
-
function
|
|
454
|
+
function checkOrdering(rows: readonly BoardRow[]): Finding[] {
|
|
405
455
|
const findings: Finding[] = []
|
|
406
456
|
const parked = rows.filter((row) => row.group === 'Needs a plan')
|
|
407
457
|
|
|
408
458
|
parked.forEach((row, index) => {
|
|
409
|
-
if (!row.ordinal) return
|
|
410
|
-
|
|
411
459
|
const position = index + 1
|
|
460
|
+
|
|
461
|
+
if (!row.ordinal) {
|
|
462
|
+
if (row.ranked) return
|
|
463
|
+
|
|
464
|
+
findings.push({
|
|
465
|
+
kind: 'row-unranked',
|
|
466
|
+
group: row.group,
|
|
467
|
+
subject: subjectOf(row),
|
|
468
|
+
message: `names no row or class it is ranked against, so its position ${position} of ${parked.length} in ${row.group} records only when it was filed.`,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
|
|
412
474
|
const expected =
|
|
413
475
|
row.ordinal === 'last'
|
|
414
476
|
? parked.length
|
|
@@ -1061,7 +1123,7 @@ export async function validateBoard(
|
|
|
1061
1123
|
...checkPlans(rows, dir, root),
|
|
1062
1124
|
...(await checkPlanAgreement(rows, dir, root)),
|
|
1063
1125
|
...checkCollisions(rows),
|
|
1064
|
-
...
|
|
1126
|
+
...checkOrdering(rows),
|
|
1065
1127
|
...parked.findings,
|
|
1066
1128
|
]
|
|
1067
1129
|
|
package/standards/tasks.md
CHANGED
|
@@ -73,7 +73,11 @@ The `Waiting on` column under `## Up next` carries that reason in one of three f
|
|
|
73
73
|
|
|
74
74
|
Under `## Up next` a collision names the file held by the task already running, a sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
|
|
75
75
|
|
|
76
|
-
Under `## Needs a plan` the cell carries two halves and each takes one clause: what the task needs before it can be planned, then why it sits at this position.
|
|
76
|
+
Under `## Needs a plan` the cell carries two halves and each takes one clause: what the task needs before it can be planned, then why it sits at this position. The second clause is comparative, and it names what the row is ranked against in one of two forms: a sibling row, or the class of rows it sits in. The row `leads`, `heads`, `opens`, `closes`, `trails`, `precedes`, `follows`, or `outranks` whichever it names, or `sits under`, `above`, or `below` it. Either form puts something other than this row in the clause, which is what makes the position readable by anyone but its author.
|
|
77
|
+
|
|
78
|
+
Why the task matters is not that claim, and it is what the cell drifts into. A reason naming no other row ranks against nothing, so every row reads defensible on its own and the order records when each was filed. The comparison replaces that reason rather than joining it, since the cell still takes two clauses and no third one is licensed by naming a rival in the second. A cell running past that is the paragraph this file already deletes, arriving one row at a time rather than all at once, and the group is where it costs the most, since it holds the rows nobody has read recently and is the longest group on any board that needs a backlog at all.
|
|
79
|
+
|
|
80
|
+
`canon tasks validate` reads that clause back, so a cell phrased comparatively and unusually reports as unranked. The vocabulary above is the whole of what it recognizes, and widening it is a change to the check rather than something the cell may decide for itself.
|
|
77
81
|
|
|
78
82
|
```markdown
|
|
79
83
|
---
|
|
@@ -96,9 +100,9 @@ description: One line on what the board covers
|
|
|
96
100
|
|
|
97
101
|
## Needs a plan
|
|
98
102
|
|
|
99
|
-
| Task | Waiting on
|
|
100
|
-
| ------------------------------- |
|
|
101
|
-
| [vXX.Y <slug>](vXX.Y-<slug>.md) | <what it needs before it can be planned, and
|
|
103
|
+
| Task | Waiting on |
|
|
104
|
+
| ------------------------------- | --------------------------------------------------------------------------------- |
|
|
105
|
+
| [vXX.Y <slug>](vXX.Y-<slug>.md) | <what it needs before it can be planned, and which row or class it ranks against> |
|
|
102
106
|
```
|
|
103
107
|
|
|
104
108
|
The tests live here so the board does not carry them. Writing them as a sentence under each heading produces the paragraph the rule above deletes, and a criterion with no home gets restated from memory every time the board is touched.
|