@erclx/canon 4.118.0 → 4.119.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-rollout/SKILL.md +2 -2
- package/claude/skills/design-extract/SKILL.md +1 -1
- package/claude/skills/docs-fold/SKILL.md +2 -2
- package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +5 -3
- package/claude/skills/memory-capture/SKILL.md +3 -3
- package/claude/skills/memory-review/SKILL.md +3 -3
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/review-branch/SKILL.md +1 -1
- package/claude/skills/review-pr/SKILL.md +1 -1
- package/claude/skills/role-planner/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +10 -41
- package/claude/skills/seed-sync/SKILL.md +1 -1
- package/claude/skills/session-compact/SKILL.md +1 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-relay/REQUIREMENT.md +7 -1
- package/claude/skills/session-relay/SKILL.md +4 -7
- package/claude/skills/session-worktree/REQUIREMENT.md +5 -1
- package/claude/skills/session-worktree/SKILL.md +7 -1
- package/claude/skills/session-worktree/references/main-root-writes.md +24 -0
- package/claude/skills/task-board/SKILL.md +4 -3
- package/claude/skills/teach-workspace/SKILL.md +4 -4
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/claude/skills/ux-walkthrough/references/relay.md +1 -1
- package/docs/agents/commands.md +2 -2
- package/docs/agents/install-and-sync.md +11 -6
- package/docs/agents/tasks.md +4 -4
- package/docs/target-projects.md +1 -1
- package/docs/workflow/ai-workflow.md +9 -9
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/renames.toml +13 -0
- package/governance/rules/core/085-worktrees.md +2 -8
- package/package.json +1 -1
- package/src/claude/cases/workflow.ts +10 -0
- package/src/commands/sync.ts +11 -3
- package/src/commands/tasks.ts +9 -8
- package/src/design/base.css +19 -19
- package/src/design/board.ts +18 -7
- package/src/design/neutral.ts +127 -0
- package/src/design/regen.ts +6 -4
- package/src/design/tokens.ts +5 -2
- package/src/gate/stages.ts +4 -4
- package/src/gov/adapter.ts +51 -1
- package/src/gov/install.ts +13 -3
- package/src/gov/renames.ts +76 -0
- package/src/gov/stacks.ts +5 -1
- package/src/sync/check.ts +19 -4
- package/src/sync/engine.ts +67 -3
- package/src/tasks/archive.ts +28 -7
- package/src/tasks/record.ts +16 -9
- package/src/tasks/validate.ts +3 -1
- package/standards/tasks.md +5 -3
- package/governance/rules/core/091-channel.md +0 -13
|
@@ -4,7 +4,7 @@ Read when the operator calls a batch, and at the start of the walkthrough when a
|
|
|
4
4
|
|
|
5
5
|
## Where the batch goes
|
|
6
6
|
|
|
7
|
-
- Send it to the controller where the session was dispatched by one,
|
|
7
|
+
- Send it to the controller where the session was dispatched by one, through `canon:session-relay`, which resolves the address and carries the send.
|
|
8
8
|
- Give it to the operator as one list in the conversation where no controller exists. The walkthrough file is the handoff either way.
|
|
9
9
|
|
|
10
10
|
## When
|
package/docs/agents/commands.md
CHANGED
|
@@ -23,10 +23,10 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
|
|
|
23
23
|
| `canon indexes list [path]` | Flatten every folder index under a path into one `{path, title, description}` catalog (`--json`) |
|
|
24
24
|
| `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
25
25
|
| `canon design render` | Render `canon/DESIGN.md` tokens to HTML and CSS, declaring each vendored face the typography table names with `--embed-fonts` |
|
|
26
|
-
| `canon design regen` | Rewrite this repository's `canon/DESIGN.md` and `src/design/base.css` from `src/design/
|
|
26
|
+
| `canon design regen` | Rewrite this repository's `canon/DESIGN.md` from `src/design/tokens.ts` and `src/design/base.css` from `src/design/neutral.ts` |
|
|
27
27
|
| `canon design board` | Generate a static page set indexing tokens, surfaces, wireframes, past candidates, components, and references against a caller-resolved `--root` (`--out`), defaulting to the main worktree |
|
|
28
28
|
| `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
|
|
29
|
-
| `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css`
|
|
29
|
+
| `canon design install` | Install the neutral base stylesheet into a project at `.claude/design/base.css` |
|
|
30
30
|
| `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
|
|
31
31
|
| `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
|
|
32
32
|
| `canon slides list` | List the available slide layouts (`--json` for the catalog) |
|
|
@@ -19,10 +19,13 @@ removes a stale `.claude/GOV.md` from the retired build. Use
|
|
|
19
19
|
|
|
20
20
|
Everything under `.claude/rules/canon/` belongs to the toolkit, so a rule
|
|
21
21
|
there that the toolkit no longer ships is deleted on sync, edited or not. That
|
|
22
|
-
is how a retired
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
is how a retired rule stops loading in a target. A renamed or renumbered rule
|
|
23
|
+
the toolkit declares in `governance/renames.toml` moves instead: the sync
|
|
24
|
+
installs the rule under its new name and deletes the old file, whether or not
|
|
25
|
+
the target's stamp records a stack. Local edits to the old file are not
|
|
26
|
+
carried, and the sync says so. The sync lists each change by path before it
|
|
27
|
+
applies, and the target's git diff is the record afterwards. `canon sync
|
|
28
|
+
--check` lists the same file as `retired` or `renamed` before any sync runs. A project's own rules belong under `.claude/rules/project/`, where
|
|
26
29
|
`canon standards rule` reserves `900-999` for them, and a rule placed under
|
|
27
30
|
`canon/` instead is lost on the next sync.
|
|
28
31
|
|
|
@@ -164,9 +167,11 @@ matches what the toolkit installed, `customized` when the project edited it,
|
|
|
164
167
|
`stranded` when it sits at a path the toolkit no longer installs to, `orphaned`
|
|
165
168
|
when the project authored it, or `drifted` when no stamp covers it. Governance
|
|
166
169
|
also reports `missing`, for a rule the target's recorded stack lists that its
|
|
167
|
-
tree does not hold at all,
|
|
170
|
+
tree does not hold at all, `retired`, for a rule under
|
|
168
171
|
`.claude/rules/canon/` the toolkit no longer ships, which the next sync
|
|
169
|
-
deletes
|
|
172
|
+
deletes, and `renamed`, for one the toolkit ships under a new name, which the
|
|
173
|
+
next sync moves. A `retired` or `renamed` file counts toward `--exit-code`,
|
|
174
|
+
since one sync clears it.
|
|
170
175
|
|
|
171
176
|
Use `--json` for the machine-readable report and `--exit-code` to fail a CI job. Orphaned
|
|
172
177
|
and missing files are both excluded from that exit code: a project-authored
|
package/docs/agents/tasks.md
CHANGED
|
@@ -50,7 +50,7 @@ canon tasks archive --pull-request 673 --json
|
|
|
50
50
|
| `--json` | Emit a machine-readable record on stdout |
|
|
51
51
|
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
52
52
|
|
|
53
|
-
Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, or `bad-input`.
|
|
53
|
+
Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, `earlier-slice`, or `bad-input`. `earlier-slice` fires when `--pull-request` names a number that is not the last on the task's `Pull request:` line, since outcomes are ticked at ship time and only the last slice's merge closes the task.
|
|
54
54
|
|
|
55
55
|
An outcome whose body is struck reads as cut rather than open or closed, whatever its checkbox holds: `- ~~<outcome>~~ <why>`. A task carrying only cut outcomes archives, since the gate refuses `no-outcomes` only when both the closed and the cut counts are zero. The success record carries `closed` and `cut` as counts, so a reader tells a shipped task from an abandoned one without opening the file.
|
|
56
56
|
|
|
@@ -245,13 +245,13 @@ canon tasks plan-link v28.1-trigger-escalation .canon/plans/feature-dispatch-ans
|
|
|
245
245
|
|
|
246
246
|
The plan resolves the same way `canon tasks plan-answers` resolves one, against the project root first and `.canon/tasks/` second, so a bare slug and a board-relative path both work. A reference resolving to no file refuses as `no-plan`, naming every base it looked under.
|
|
247
247
|
|
|
248
|
-
The write
|
|
248
|
+
The write adds the line when it is absent and corrects the target in place when it exists, anchored on the H1 rather than on the last origin line, since `Plan:` is the first origin line a task carries rather than the last. The `action` field reports `added`, `corrected`, or `unchanged`, which makes a rerun against the same plan safe.
|
|
249
249
|
|
|
250
250
|
Exit codes: `0` recorded, `1` refused. The `reason` field carries `no-board`, `no-match`, `no-plan`, or `bad-input`.
|
|
251
251
|
|
|
252
252
|
## Pull request
|
|
253
253
|
|
|
254
|
-
`canon tasks pull-request` records the number a branch's pull request carries onto the task that branch closes. It adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already holds,
|
|
254
|
+
`canon tasks pull-request` records the number a branch's pull request carries onto the task that branch closes. It adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already holds. When the line exists it appends `, #NNN` rather than replacing it, so a task shipped in slices lists every pull request oldest first, and a number already listed leaves the line alone. A line that does not read as a comma-separated list of `#NNN` entries is replaced whole.
|
|
255
255
|
|
|
256
256
|
Name the task by its filename stem, or by the plan its `Plan:` line points at:
|
|
257
257
|
|
|
@@ -266,7 +266,7 @@ canon tasks pull-request 673 --plan worktree-scratch-routing --json
|
|
|
266
266
|
| `--json` | Emit a machine-readable record on stdout |
|
|
267
267
|
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
268
268
|
|
|
269
|
-
A plan is matched on the token both spellings share, so `worktree-scratch-routing`, `feature-worktree-scratch-routing`, and `.canon/plans/feature-worktree-scratch-routing.md` all select the same task. The `action` field reports `added`, `corrected`, or `unchanged`, which makes a rerun against the same number safe.
|
|
269
|
+
A plan is matched on the token both spellings share, so `worktree-scratch-routing`, `feature-worktree-scratch-routing`, and `.canon/plans/feature-worktree-scratch-routing.md` all select the same task. The `action` field reports `added`, `appended`, `corrected`, or `unchanged`, which makes a rerun against the same number safe.
|
|
270
270
|
|
|
271
271
|
Exit codes: `0` recorded, `1` refused. The `reason` field carries `no-board`, `no-match`, or `ambiguous`. `git-pr` skips silently on those three, because each is a case where a guessed write would archive the wrong task once the branch merges.
|
|
272
272
|
|
package/docs/target-projects.md
CHANGED
|
@@ -225,7 +225,7 @@ The report opens by naming the binary running it. The installed version reads ag
|
|
|
225
225
|
|
|
226
226
|
#### Then the causes
|
|
227
227
|
|
|
228
|
-
A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `canon:seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is a relocation the report names but no command runs. A `retired` rule is one under `.claude/rules/canon/` the toolkit no longer ships, and the next `canon gov sync` deletes it.
|
|
228
|
+
A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `canon:seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is a relocation the report names but no command runs. A `retired` rule is one under `.claude/rules/canon/` the toolkit no longer ships, and the next `canon gov sync` deletes it. A `renamed` rule is one the toolkit now ships under a new name, and the next sync installs the new name and deletes the old file.
|
|
229
229
|
|
|
230
230
|
That attribution comes from `canon/config/config.json`, which every install and sync now writes, falling back to `.claude/canon/config.json` for a target stamped before that move. A target stamped before that path shipped is read from the retired `.claude/canon.json` instead, reported rather than migrated. Governance records a hash per installed file, plus the stack `canon gov install` was given, and tooling records the stack chain it resolved instead of any file hash, since its install runs no per-file walk to attribute.
|
|
231
231
|
|
|
@@ -224,15 +224,15 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
224
224
|
|
|
225
225
|
### Build the feature
|
|
226
226
|
|
|
227
|
-
| Skill | When to use
|
|
228
|
-
| ---------------------------- |
|
|
229
|
-
| `canon:session-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch
|
|
230
|
-
| `canon:auto-ship` | After plan approval, to chain implement, verify, review, draft PR
|
|
231
|
-
| `canon:project-commands` | When the project's own command needs running
|
|
232
|
-
| `canon:test-first` | Before implementing a planned change, to run its test red, green, then refactor
|
|
233
|
-
| `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts
|
|
234
|
-
| `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first
|
|
235
|
-
| `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested
|
|
227
|
+
| Skill | When to use |
|
|
228
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `canon:session-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch, or to route a refused main-root write |
|
|
230
|
+
| `canon:auto-ship` | After plan approval, to chain implement, verify, review, draft PR |
|
|
231
|
+
| `canon:project-commands` | When the project's own command needs running |
|
|
232
|
+
| `canon:test-first` | Before implementing a planned change, to run its test red, green, then refactor |
|
|
233
|
+
| `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts |
|
|
234
|
+
| `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
|
|
235
|
+
| `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested |
|
|
236
236
|
|
|
237
237
|
### Check the work before it leaves the branch
|
|
238
238
|
|
|
@@ -24,7 +24,7 @@ The toolkit seed in `tooling/claude/seeds/canon/DESIGN.md` ships a token-table t
|
|
|
24
24
|
|
|
25
25
|
The `canon:design-extract` skill drafts the file, sourcing tokens from a project's existing prose and CLI UI surfaces, or proposing them from `canon/REQUIREMENTS.md` and a `## Personality` paragraph when no UI code exists yet. `canon design render` writes an HTML plus CSS preview to `.canon/tmp/render/design/` for eyeballing the current system without leaving Claude Code. See `canon/context/design.md`.
|
|
26
26
|
|
|
27
|
-
A project wanting
|
|
27
|
+
A project wanting a neutral starting point to override runs `canon design install`, which copies one stylesheet to `.claude/design/base.css` carrying an achromatic token set as custom properties and two components built on them. Greys run from near-black to near-white and the accent is monochrome, so the only palette the project renders is one it writes itself. That file is toolkit-owned and `canon design sync` refreshes it, so a project overrides a value in `.claude/design/project/` instead, which sync never touches. Nothing arrives without that install, and the two channels are independent: a record drafted by the extract skill is the project's own, and the installed stylesheet is the toolkit's.
|
|
28
28
|
|
|
29
29
|
A cell no source anchors ends in `? verify`, and the preview shows that marker beside the value rather than folding it in, so a swatch and a font sample stay built from the value alone. A confidence line above the sections names how many cells are anchored against how many are tagged, which is what tells a reader whether they are looking at a record of the code or a proposal about it. It reads the columns a source could anchor and leaves out the row names, so the ratio is not diluted by cells no tag could ever reach. The proposal path tags nearly all of them, so that count reads low on day one by design.
|
|
30
30
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Rule renames a sync carries a target across. Each key under [renamed] is an
|
|
2
|
+
# old rule basename and its value the basename the same rule ships under now,
|
|
3
|
+
# so `canon gov sync` replaces a held old file with the rule under its new name
|
|
4
|
+
# rather than only deleting it.
|
|
5
|
+
#
|
|
6
|
+
# An entry declares one-to-one identity and nothing else. A fold of several
|
|
7
|
+
# rules into one, or a split of one into several, is a retirement plus an
|
|
8
|
+
# addition and takes no entry here. Add the entry in the same change as the
|
|
9
|
+
# rename or renumber, and never prune one: a target can sit any number of
|
|
10
|
+
# releases behind, and a chain A to B to C resolves A only while both links
|
|
11
|
+
# stay. A key must name no live rule and every chain must end on one, which
|
|
12
|
+
# `src/gov/renames.test.ts` checks against this file.
|
|
13
|
+
[renamed]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Enter a linked worktree for implementation, resolve shared session scratch at the main root, and route a refused main-root write through session-worktree
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Worktrees standards
|
|
@@ -11,10 +11,4 @@ description: Route tracked-file writes and shared session scratch correctly from
|
|
|
11
11
|
## Shared session scratch
|
|
12
12
|
|
|
13
13
|
- Shared session scratch (`.canon/plans/`, `.canon/review/`, `.canon/memory/`, `.canon/tasks/`) lives at the main worktree root, not inside a linked worktree. From a linked worktree, resolve these paths against the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`. Fall back to `pwd` if not a git repo.
|
|
14
|
-
-
|
|
15
|
-
- From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`.
|
|
16
|
-
- From a linked worktree, `Edit` and `Write` are refused for every main-root path, session scratch included. The refusal names session isolation and points at the worktree copy, which is a second gitignored file no later session reads, so never take that redirect.
|
|
17
|
-
- `Read` resolves against the main root normally from a linked worktree. A main-root write reaches it only through `Bash`, as one plain command rather than a compound one, which is refused for complexity.
|
|
18
|
-
- Route a main-root write by what it does to the file. Creating a whole file goes out as one plain `Bash` command carrying a heredoc. Changing a line inside a file that already exists goes through a command that resolves the main root in-process, because the shell route for that case is the stream editor this file bans.
|
|
19
|
-
- A background session that entered no worktree at all meets the identical refusal writing to the main root, since the guard gates on isolation rather than on sitting inside a linked worktree. A planner writing its plan file from no worktree takes the same heredoc route for that reason.
|
|
20
|
-
- The heredoc route costs nothing on `.canon/plans/` and `.canon/review/`, which no `PostToolUse` hook watches, and costs the index on `.canon/tasks/` and `.canon/memory/`, since a hook matching `Write|Edit|MultiEdit` never fires on `Bash`. Regenerate positionally: in-process where a verb such as `canon tasks archive` already calls it, elsewhere by an explicit `canon indexes regen` call.
|
|
14
|
+
- From a linked worktree, or from a background session that entered none, `Edit` and `Write` refuse a main-root path and redirect to a worktree copy no later session reads. Never take that redirect. Route the write the way the `session-worktree` skill states, and report it rather than proceeding silently when that skill does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
|
package/package.json
CHANGED
|
@@ -163,11 +163,21 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
163
163
|
prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
|
|
164
164
|
expect: 'session-worktree',
|
|
165
165
|
},
|
|
166
|
+
{
|
|
167
|
+
prompt:
|
|
168
|
+
'My Write to the plan file at the main root got refused for session isolation. How do I land it there?',
|
|
169
|
+
expect: 'session-worktree',
|
|
170
|
+
},
|
|
166
171
|
{
|
|
167
172
|
prompt:
|
|
168
173
|
'Relay this over to the session that dispatched me. It gave me a sessionId rather than a name, so work out who that is and send it.',
|
|
169
174
|
expect: 'session-relay',
|
|
170
175
|
},
|
|
176
|
+
{
|
|
177
|
+
prompt:
|
|
178
|
+
'I am stuck on a question only the operator can answer. Before I stop and ask it, tell the session that dispatched me, it gave me a sessionId.',
|
|
179
|
+
expect: 'session-relay',
|
|
180
|
+
},
|
|
171
181
|
|
|
172
182
|
// Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
|
|
173
183
|
{
|
package/src/commands/sync.ts
CHANGED
|
@@ -133,9 +133,17 @@ function renderCheck(report: CheckReport): void {
|
|
|
133
133
|
logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const {
|
|
137
|
+
stale,
|
|
138
|
+
customized,
|
|
139
|
+
drifted,
|
|
140
|
+
stranded,
|
|
141
|
+
retired,
|
|
142
|
+
renamed,
|
|
143
|
+
orphaned,
|
|
144
|
+
missing,
|
|
145
|
+
} = domain.counts
|
|
146
|
+
if (stale + customized + drifted + stranded + retired + renamed === 0) {
|
|
139
147
|
logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
|
|
140
148
|
}
|
|
141
149
|
// A local file is not a deficiency and a missing rule is, so this never
|
package/src/commands/tasks.ts
CHANGED
|
@@ -409,12 +409,14 @@ export function register(program: Command): void {
|
|
|
409
409
|
[
|
|
410
410
|
'',
|
|
411
411
|
'Exit codes:',
|
|
412
|
-
' 0 the line was added, corrected, or already correct',
|
|
412
|
+
' 0 the line was added, appended to, corrected, or already correct',
|
|
413
413
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
414
414
|
'',
|
|
415
415
|
'It adds Pull request: #NNN under the origin lines the task carries,',
|
|
416
|
-
'and
|
|
417
|
-
'
|
|
416
|
+
'and appends the number to the line when one exists, so a task shipped',
|
|
417
|
+
'in slices lists every pull request. A line that does not read as a',
|
|
418
|
+
'list is replaced. The board is shared scratch, so a linked worktree',
|
|
419
|
+
'records against the same board.',
|
|
418
420
|
'',
|
|
419
421
|
'Examples:',
|
|
420
422
|
' canon tasks pull-request 673 v28.1-trigger-escalation',
|
|
@@ -448,11 +450,10 @@ export function register(program: Command): void {
|
|
|
448
450
|
' 0 the line was added, corrected, or already correct',
|
|
449
451
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
450
452
|
'',
|
|
451
|
-
'It writes Plan: [<label>](<target>) right after the H1, the
|
|
452
|
-
'
|
|
453
|
-
'
|
|
454
|
-
'
|
|
455
|
-
'resolves the board root in-process.',
|
|
453
|
+
'It writes Plan: [<label>](<target>) right after the H1, corrects the',
|
|
454
|
+
'target in place when the line exists, and resolves both a bare slug',
|
|
455
|
+
'and a board-relative path the way canon tasks plan-answers does. Safe',
|
|
456
|
+
'from a linked worktree, since it resolves the board root in-process.',
|
|
456
457
|
'',
|
|
457
458
|
'Examples:',
|
|
458
459
|
' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
|
package/src/design/base.css
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/* Generated by `canon design regen` from src/design/
|
|
1
|
+
/* Generated by `canon design regen` from src/design/neutral.ts. Do not edit.
|
|
2
2
|
This is the base half of a design install. A target overrides a value in
|
|
3
3
|
.claude/design/project/, which the three-way merge leaves alone, so this
|
|
4
4
|
file changes freely and nothing a project wrote lives in it. */
|
|
5
5
|
|
|
6
6
|
:root {
|
|
7
|
-
--color-background: #
|
|
8
|
-
--color-surface: #
|
|
9
|
-
--color-chrome: #
|
|
10
|
-
--color-border: #
|
|
11
|
-
--color-text: #
|
|
12
|
-
--color-text-body: #
|
|
13
|
-
--color-text-secondary: #
|
|
14
|
-
--color-muted: #
|
|
15
|
-
--color-accent: #
|
|
7
|
+
--color-background: #0e0e0e;
|
|
8
|
+
--color-surface: #141414;
|
|
9
|
+
--color-chrome: #1a1a1a;
|
|
10
|
+
--color-border: #292929;
|
|
11
|
+
--color-text: #dddddd;
|
|
12
|
+
--color-text-body: #b2b2b2;
|
|
13
|
+
--color-text-secondary: #8e8e8e;
|
|
14
|
+
--color-muted: #7f7f7f;
|
|
15
|
+
--color-accent: #ececec;
|
|
16
16
|
--color-success: #61c454;
|
|
17
|
-
--color-light-background: #
|
|
18
|
-
--color-light-surface: #
|
|
19
|
-
--color-light-chrome: #
|
|
20
|
-
--color-light-text: #
|
|
21
|
-
--color-light-text-body: #
|
|
22
|
-
--color-light-text-secondary: #
|
|
23
|
-
--color-light-muted: #
|
|
24
|
-
--color-light-accent: #
|
|
17
|
+
--color-light-background: #fafafa;
|
|
18
|
+
--color-light-surface: #f1f1f1;
|
|
19
|
+
--color-light-chrome: #e8e8e8;
|
|
20
|
+
--color-light-text: #252525;
|
|
21
|
+
--color-light-text-body: #444444;
|
|
22
|
+
--color-light-text-secondary: #5f5f5f;
|
|
23
|
+
--color-light-muted: #6c6c6c;
|
|
24
|
+
--color-light-accent: #191919;
|
|
25
25
|
--color-light-success: #2d6b22;
|
|
26
|
-
--color-light-border: #
|
|
26
|
+
--color-light-border: #d4d4d4;
|
|
27
27
|
--space-xs: 0.25rem;
|
|
28
28
|
--space-sm: 0.5rem;
|
|
29
29
|
--space-md: 0.875rem;
|
package/src/design/board.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { join, relative, sep } from 'node:path'
|
|
11
11
|
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
12
12
|
import { renderDesignDoc } from '@/design/render'
|
|
13
|
+
import { NEUTRAL_TOKENS } from '@/design/neutral'
|
|
13
14
|
import { colorValue } from '@/design/tokens'
|
|
14
15
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
15
16
|
import { recordDir } from '@/record-root'
|
|
@@ -131,6 +132,16 @@ const SCROLLSPY_SCRIPT = [
|
|
|
131
132
|
'})();</script>',
|
|
132
133
|
].join('')
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* A role's toolkit value, falling back to the neutral install base's where the
|
|
137
|
+
* toolkit record drops the role, so the board never restates a hex of its own.
|
|
138
|
+
*/
|
|
139
|
+
function roleColor(role: string): string {
|
|
140
|
+
const value = colorValue(role) ?? colorValue(role, NEUTRAL_TOKENS)
|
|
141
|
+
if (value === undefined) throw new Error(`No design token declares ${role}`)
|
|
142
|
+
return value
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
/**
|
|
135
146
|
* The same brand mark `src/design/render.ts` embeds in its own preview,
|
|
136
147
|
* colored with the dark accent since the board's default chrome is dark
|
|
@@ -139,7 +150,7 @@ const SCROLLSPY_SCRIPT = [
|
|
|
139
150
|
* copies rather than a shared icon helper for the first second caller.
|
|
140
151
|
*/
|
|
141
152
|
function faviconLink(): string {
|
|
142
|
-
const color =
|
|
153
|
+
const color = roleColor('accent')
|
|
143
154
|
const href = `data:image/svg+xml,${encodeURIComponent(
|
|
144
155
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${color}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${color}" /></svg>`,
|
|
145
156
|
)}`
|
|
@@ -232,12 +243,12 @@ ${SCROLLSPY_SCRIPT}
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
function panelPage(title: string, body: string): string {
|
|
235
|
-
const accent =
|
|
236
|
-
const surface =
|
|
237
|
-
const border =
|
|
238
|
-
const muted =
|
|
239
|
-
const text =
|
|
240
|
-
const background =
|
|
246
|
+
const accent = roleColor('light-accent')
|
|
247
|
+
const surface = roleColor('light-surface')
|
|
248
|
+
const border = roleColor('light-border')
|
|
249
|
+
const muted = roleColor('light-muted')
|
|
250
|
+
const text = roleColor('light-text')
|
|
251
|
+
const background = roleColor('light-background')
|
|
241
252
|
|
|
242
253
|
return `<!doctype html>
|
|
243
254
|
<html lang="en">
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ColorToken, DesignTokens } from '@/design/tokens'
|
|
2
|
+
import { TOKENS } from '@/design/tokens'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The token set `canon design install` hands a target, achromatic so the only
|
|
6
|
+
* palette a target renders is one it wrote under `.claude/design/project/`.
|
|
7
|
+
* Type, spacing, and radii are this repository's by spread, and only the color
|
|
8
|
+
* list is a second copy, held to the same roles by `neutral.test.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Grounds, surface, chrome, and border take the OKLCH lightness of the toolkit
|
|
11
|
+
* role they replace at chroma 0. Text roles are solved against `surface` for
|
|
12
|
+
* the contrast targets `TOKENS` records (13.5, 8.6, 5.6, 4.6), and the accent
|
|
13
|
+
* for 15.5, past `text` so it still reads apart from body copy. The state
|
|
14
|
+
* roles keep the toolkit's values, since a state color is a meaning rather
|
|
15
|
+
* than a palette.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DARK_GROUNDS = ['background', 'surface'] as const
|
|
19
|
+
const LIGHT_GROUNDS = ['light-background', 'light-surface'] as const
|
|
20
|
+
|
|
21
|
+
function toolkitRole(role: string): ColorToken {
|
|
22
|
+
const token = TOKENS.color.find((candidate) => candidate.role === role)
|
|
23
|
+
if (token === undefined) throw new Error(`TOKENS declares no ${role} role`)
|
|
24
|
+
return token
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const NEUTRAL_COLOR: readonly ColorToken[] = [
|
|
28
|
+
{ role: 'background', intent: 'page canvas', value: '#0e0e0e' },
|
|
29
|
+
{
|
|
30
|
+
role: 'surface',
|
|
31
|
+
intent: 'cards, panels, raised blocks',
|
|
32
|
+
value: '#141414',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
role: 'chrome',
|
|
36
|
+
intent: 'the window titlebar, one step above the canvas',
|
|
37
|
+
value: '#1a1a1a',
|
|
38
|
+
},
|
|
39
|
+
{ role: 'border', intent: 'every rule and panel edge', value: '#292929' },
|
|
40
|
+
{
|
|
41
|
+
role: 'text',
|
|
42
|
+
intent: 'headings, counts, emphasized runs',
|
|
43
|
+
value: '#dddddd',
|
|
44
|
+
grounds: DARK_GROUNDS,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: 'text-body',
|
|
48
|
+
intent: 'default body copy',
|
|
49
|
+
value: '#b2b2b2',
|
|
50
|
+
grounds: DARK_GROUNDS,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
role: 'text-secondary',
|
|
54
|
+
intent: 'labels, captions, supporting copy',
|
|
55
|
+
value: '#8e8e8e',
|
|
56
|
+
grounds: DARK_GROUNDS,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
role: 'muted',
|
|
60
|
+
intent: 'the faintest step, trailing notes',
|
|
61
|
+
value: '#7f7f7f',
|
|
62
|
+
grounds: DARK_GROUNDS,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
role: 'accent',
|
|
66
|
+
intent: 'install command, mark, primary action',
|
|
67
|
+
value: '#ececec',
|
|
68
|
+
grounds: DARK_GROUNDS,
|
|
69
|
+
},
|
|
70
|
+
toolkitRole('success'),
|
|
71
|
+
toolkitRole('warning'),
|
|
72
|
+
toolkitRole('error'),
|
|
73
|
+
{
|
|
74
|
+
role: 'light-background',
|
|
75
|
+
intent: 'page canvas on a light ground',
|
|
76
|
+
value: '#fafafa',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
role: 'light-surface',
|
|
80
|
+
intent: 'cards and panels on a light ground',
|
|
81
|
+
value: '#f1f1f1',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
role: 'light-chrome',
|
|
85
|
+
intent: 'the window titlebar, one step above the canvas',
|
|
86
|
+
value: '#e8e8e8',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
role: 'light-text',
|
|
90
|
+
intent: 'primary text on a light ground',
|
|
91
|
+
value: '#252525',
|
|
92
|
+
grounds: LIGHT_GROUNDS,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
role: 'light-text-body',
|
|
96
|
+
intent: 'default body copy on a light ground',
|
|
97
|
+
value: '#444444',
|
|
98
|
+
grounds: LIGHT_GROUNDS,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: 'light-text-secondary',
|
|
102
|
+
intent: 'labels, captions, supporting copy on a light ground',
|
|
103
|
+
value: '#5f5f5f',
|
|
104
|
+
grounds: LIGHT_GROUNDS,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
role: 'light-muted',
|
|
108
|
+
intent: 'secondary text on a light ground',
|
|
109
|
+
value: '#6c6c6c',
|
|
110
|
+
grounds: LIGHT_GROUNDS,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
role: 'light-accent',
|
|
114
|
+
intent: 'links and primary action on light',
|
|
115
|
+
value: '#191919',
|
|
116
|
+
grounds: LIGHT_GROUNDS,
|
|
117
|
+
},
|
|
118
|
+
toolkitRole('light-success'),
|
|
119
|
+
{
|
|
120
|
+
role: 'light-border',
|
|
121
|
+
intent: 'rules and panel edges on light',
|
|
122
|
+
value: '#d4d4d4',
|
|
123
|
+
verify: true,
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
export const NEUTRAL_TOKENS: DesignTokens = { ...TOKENS, color: NEUTRAL_COLOR }
|
package/src/design/regen.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { mkdirSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { buildDesignCss } from '@/design/css'
|
|
4
4
|
import { renderDesignDocument } from '@/design/document'
|
|
5
|
+
import { NEUTRAL_TOKENS } from '@/design/neutral'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* The two artifacts the token
|
|
8
|
-
* and asserted for drift by the `design` stage of `bun run check`.
|
|
8
|
+
* The two artifacts the token sources produce, written by `canon design regen`
|
|
9
|
+
* and asserted for drift by the `design` stage of `bun run check`. The
|
|
10
|
+
* document renders from `TOKENS` and the stylesheet from `NEUTRAL_TOKENS`.
|
|
9
11
|
*
|
|
10
12
|
* The document is what a person reads and the stylesheet is what a target
|
|
11
13
|
* installs, and neither is edited by hand. That is the cost the operator took
|
|
@@ -26,7 +28,7 @@ export const DESIGN_DOCUMENT = join('canon', 'DESIGN.md')
|
|
|
26
28
|
export const DESIGN_BASE_CSS = join('src', 'design', 'base.css')
|
|
27
29
|
|
|
28
30
|
const BASE_BANNER = [
|
|
29
|
-
'Generated by `canon design regen` from src/design/
|
|
31
|
+
'Generated by `canon design regen` from src/design/neutral.ts. Do not edit.',
|
|
30
32
|
'This is the base half of a design install. A target overrides a value in',
|
|
31
33
|
'.claude/design/project/, which the three-way merge leaves alone, so this',
|
|
32
34
|
'file changes freely and nothing a project wrote lives in it.',
|
|
@@ -50,7 +52,7 @@ export function regenDesign(root: string): RegenResult {
|
|
|
50
52
|
cssPath: write(
|
|
51
53
|
root,
|
|
52
54
|
DESIGN_BASE_CSS,
|
|
53
|
-
buildDesignCss(
|
|
55
|
+
buildDesignCss(NEUTRAL_TOKENS, { banner: BASE_BANNER }),
|
|
54
56
|
),
|
|
55
57
|
}
|
|
56
58
|
}
|
package/src/design/tokens.ts
CHANGED
|
@@ -386,8 +386,11 @@ export const TOKENS: DesignTokens = {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/** A role's value, or `undefined` where the record declares no such role. */
|
|
389
|
-
export function colorValue(
|
|
390
|
-
|
|
389
|
+
export function colorValue(
|
|
390
|
+
role: string,
|
|
391
|
+
tokens: DesignTokens = TOKENS,
|
|
392
|
+
): string | undefined {
|
|
393
|
+
return tokens.color.find((token) => token.role === role)?.value
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
/**
|
package/src/gate/stages.ts
CHANGED
|
@@ -209,10 +209,10 @@ export const STAGES: readonly Stage[] = [
|
|
|
209
209
|
success: 'Hero clean',
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
// `canon/DESIGN.md`, the
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// and a render step that has to run is only safe while something fails
|
|
212
|
+
// `canon/DESIGN.md`, the web stylesheet, and the tab icon are written
|
|
213
|
+
// from `src/design/tokens.ts`, the base stylesheet from
|
|
214
|
+
// `src/design/neutral.ts`, and none is edited by hand. Four artifacts
|
|
215
|
+
// from two token sources is the cost of the token move, and a render step that has to run is only safe while something fails
|
|
216
216
|
// when it did not, which is this.
|
|
217
217
|
id: 'design',
|
|
218
218
|
label: 'Design',
|