@erclx/canon 4.69.0 → 4.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/SKILL.md +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +4 -0
  5. package/claude/skills/deploy-cloudflare/REQUIREMENT.md +37 -0
  6. package/claude/skills/deploy-cloudflare/SKILL.md +61 -0
  7. package/claude/skills/draft-and-pick/REQUIREMENT.md +1 -1
  8. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  9. package/claude/skills/{identity → draft-identity}/REQUIREMENT.md +2 -2
  10. package/claude/skills/{identity → draft-identity}/SKILL.md +2 -2
  11. package/claude/skills/{canon-screencast → draft-screencast}/REQUIREMENT.md +4 -4
  12. package/claude/skills/{canon-screencast → draft-screencast}/SKILL.md +4 -4
  13. package/claude/skills/{canon-slides-draft → draft-slides}/REQUIREMENT.md +3 -3
  14. package/claude/skills/{canon-slides-draft → draft-slides}/SKILL.md +2 -2
  15. package/claude/skills/{canon-frames-read → read-frames}/REQUIREMENT.md +2 -2
  16. package/claude/skills/{canon-frames-read → read-frames}/SKILL.md +3 -3
  17. package/claude/skills/{canon-record → record-screencast}/REQUIREMENT.md +5 -5
  18. package/claude/skills/{canon-record → record-screencast}/SKILL.md +4 -4
  19. package/claude/skills/review-pr/SKILL.md +55 -7
  20. package/claude/skills/role-orchestrator/SKILL.md +2 -1
  21. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +7 -3
  22. package/claude/skills/role-orchestrator/scripts/poll.sh +79 -34
  23. package/claude/skills/role-worker/SKILL.md +2 -1
  24. package/claude/skills/ux-audit/SKILL.md +3 -0
  25. package/docs/agents/commands.md +2 -0
  26. package/docs/agents/demo.md +3 -3
  27. package/docs/agents/index.md +1 -1
  28. package/docs/agents/pr-reads.md +47 -12
  29. package/docs/agents/tasks.md +46 -3
  30. package/docs/workflow/ai-workflow.md +19 -19
  31. package/docs/workflow/visual-design-workflow.md +1 -1
  32. package/package.json +3 -2
  33. package/scripts/core/regen-web-previews.ts +94 -0
  34. package/src/claude/cases/misc.ts +5 -1
  35. package/src/claude/cases/workflow.ts +4 -4
  36. package/src/commands/demo.ts +1 -1
  37. package/src/commands/pr.ts +130 -1
  38. package/src/commands/tasks.ts +178 -1
  39. package/src/commands/teach.ts +2 -0
  40. package/src/demo/beats.ts +1 -1
  41. package/src/gate/measures.ts +66 -0
  42. package/src/gate/stages.ts +11 -0
  43. package/src/migrate/skill-names.ts +25 -1
  44. package/src/pr/review-scope.ts +177 -0
  45. package/src/tasks/archive.ts +206 -3
  46. package/src/tasks/label.ts +14 -6
  47. package/src/tasks/validate.ts +22 -0
  48. package/src/teach/workspace.ts +60 -23
  49. package/standards/plan.md +1 -1
  50. package/standards/tasks.md +15 -1
  51. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +103 -0
  52. package/tooling/cloudflare/manifest.toml +5 -0
  53. package/tooling/cloudflare/reference.md +24 -0
@@ -42,9 +42,20 @@ export const TEACH_ASSETS = 'assets'
42
42
  * The one stylesheet every lesson in a workspace links. The name is fixed here
43
43
  * rather than chosen per lesson, because the second lesson has to reach the
44
44
  * file the first one wrote and a name composed twice is a name that can differ.
45
+ * This file is workspace-owned: `writeStylesheet` writes it once and leaves it
46
+ * alone, since a lesson adds its own rules here. It imports `TEACH_STYLESHEET_BASE`
47
+ * for the tokens and components, which is the half a resync may always replace.
45
48
  */
46
49
  export const TEACH_STYLESHEET = 'course.css'
47
50
 
51
+ /**
52
+ * The seeded half of a workspace's styling, imported by `TEACH_STYLESHEET`.
53
+ * `writeStylesheet` rewrites this file from the design source on every call,
54
+ * since nothing workspace-authored lives in it, which is what lets a design
55
+ * token move without a `--force` flag discarding a workspace's lesson rules.
56
+ */
57
+ export const TEACH_STYLESHEET_BASE = 'base.css'
58
+
48
59
  /**
49
60
  * The mission heading whose list a session reads as exit criteria. The writer,
50
61
  * the reader below, and the record validator all match this one spelling, so a
@@ -1001,31 +1012,40 @@ export interface StylesheetWritten {
1001
1012
  readonly slug: string
1002
1013
  /** Relative to the root, so a caller prints a path a reader can open. */
1003
1014
  readonly path: string
1004
- /** False when the workspace already held one and this call left it alone. */
1015
+ /** Relative to the root, the re-synced seed `path` imports. */
1016
+ readonly basePath: string
1017
+ /** False when the workspace already held `path` and this call left it alone. */
1005
1018
  readonly written: boolean
1006
1019
  }
1007
1020
 
1008
1021
  export type StylesheetOutcome = StylesheetWritten | TeachRefused
1009
1022
 
1010
- const STYLESHEET_BANNER = [
1023
+ const STYLESHEET_BASE_BANNER = [
1011
1024
  'Seeded by `canon teach stylesheet` from the design source in',
1012
- 'src/design/tokens.ts. The tokens and the two components below are the',
1013
- 'system this workspace renders in. Add lesson rules under them and read a',
1014
- 'value through its custom property rather than restating the hex, which is',
1015
- 'what let one workspace fork the palette from every other.',
1025
+ 'src/design/tokens.ts, and rewritten on every call. The tokens and the two',
1026
+ 'components below are the system this workspace renders in. Read a value',
1027
+ 'through its custom property rather than restating the hex, which is what',
1028
+ 'let one workspace fork the palette from every other. Add lesson rules to',
1029
+ `${TEACH_STYLESHEET} instead, which imports this file and is never rewritten.`,
1016
1030
  ].join('\n ')
1017
1031
 
1032
+ const stylesheetSeed = (): string =>
1033
+ `@import url('./${TEACH_STYLESHEET_BASE}');\n`
1034
+
1018
1035
  /**
1019
- * Writes a workspace's one stylesheet from the design source.
1036
+ * Writes a workspace's stylesheet pair from the design source.
1020
1037
  *
1021
- * Every workspace used to carry a hand-authored copy, which is how the course
1022
- * palette forked once per workspace. The name is fixed at `TEACH_STYLESHEET`
1023
- * and the folder at `TEACH_ASSETS` for the same reason a second lesson has to
1024
- * reach the file the first one wrote, and this is what puts the values in it.
1038
+ * Every workspace used to carry one hand-authored file, which is how the
1039
+ * course palette forked once per workspace. The names are fixed at
1040
+ * `TEACH_STYLESHEET` and `TEACH_STYLESHEET_BASE`, and the folder at
1041
+ * `TEACH_ASSETS`, for the same reason a second lesson has to reach the files
1042
+ * the first one wrote and names composed twice can differ.
1025
1043
  *
1026
- * An existing stylesheet is left alone rather than replaced. A workspace adds
1027
- * lesson rules to this file as it goes, so overwriting would discard them, and
1028
- * `--force` is the caller saying it wants the seed back.
1044
+ * `TEACH_STYLESHEET_BASE` carries the tokens and the components and is
1045
+ * rewritten every call, since nothing workspace-authored lives in it.
1046
+ * `TEACH_STYLESHEET` imports it, carries a workspace's own lesson rules, and
1047
+ * is written once and left alone, since overwriting it would discard them.
1048
+ * `--force` is the caller saying it wants that file's seed back too.
1029
1049
  */
1030
1050
  export async function writeStylesheet(
1031
1051
  root: string,
@@ -1036,22 +1056,39 @@ export async function writeStylesheet(
1036
1056
  if (!found.ok) return found
1037
1057
 
1038
1058
  const workspace = found.workspace
1039
- const rel = join(workspace.path, TEACH_ASSETS, TEACH_STYLESHEET)
1059
+ const assetsDir = join(workspace.path, TEACH_ASSETS)
1060
+ const baseRel = join(assetsDir, TEACH_STYLESHEET_BASE)
1061
+ const rel = join(assetsDir, TEACH_STYLESHEET)
1062
+ const basePath = join(root, baseRel)
1040
1063
  const path = join(root, rel)
1041
1064
 
1042
- if (existsSync(path) && !force) {
1043
- return { ok: true, slug: workspace.slug, path: rel, written: false }
1044
- }
1045
-
1046
- await mkdir(join(root, workspace.path, TEACH_ASSETS), { recursive: true })
1065
+ await mkdir(join(root, assetsDir), { recursive: true })
1047
1066
  await writeFile(
1048
- path,
1067
+ basePath,
1049
1068
  buildDesignCss(undefined, {
1050
- banner: STYLESHEET_BANNER,
1069
+ banner: STYLESHEET_BASE_BANNER,
1051
1070
  embedFonts: TEACH_FONT_FACES,
1052
1071
  components: TEACH_STYLESHEET_COMPONENTS,
1053
1072
  }),
1054
1073
  )
1055
1074
 
1056
- return { ok: true, slug: workspace.slug, path: rel, written: true }
1075
+ if (existsSync(path) && !force) {
1076
+ return {
1077
+ ok: true,
1078
+ slug: workspace.slug,
1079
+ path: rel,
1080
+ basePath: baseRel,
1081
+ written: false,
1082
+ }
1083
+ }
1084
+
1085
+ await writeFile(path, stylesheetSeed())
1086
+
1087
+ return {
1088
+ ok: true,
1089
+ slug: workspace.slug,
1090
+ path: rel,
1091
+ basePath: baseRel,
1092
+ written: true,
1093
+ }
1057
1094
  }
package/standards/plan.md CHANGED
@@ -124,7 +124,7 @@ This contract inverts the one an intake folder keeps, where an empty slot means
124
124
  - Write the plan before implementation starts, and treat it as the scope of the run that executes it.
125
125
  - Keep every plan at one root. A plan copied into each parallel working tree forks, and the copies answer the same question differently.
126
126
  - Amend the plan in place when a decision changes mid-flight. Do not append a second passage narrating the change, which leaves a reader to work out which of two answers is current. An execution-time deviation from a suggestion is one such amendment, and the contract above fixes which line takes it.
127
- - Move the plan to `.canon/plans/archive/` when the work it describes ships. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it. The archive nests inside `.canon/plans/` rather than sitting beside it as `.claude/plans-archive/`, so an older layout is a flat sibling by that name and a current one is not.
127
+ - Move the plan to `.canon/plans/archive/` when the work it describes ships or is declined. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it. The archive nests inside `.canon/plans/` rather than sitting beside it as `.claude/plans-archive/`, so an older layout is a flat sibling by that name and a current one is not.
128
128
  - Write the plan in the same session that opens the task it serves. The session executing it later inherits reasoning it would otherwise re-derive.
129
129
 
130
130
  ## Anti-patterns
@@ -31,6 +31,8 @@ Does not govern:
31
31
  ├── priority.md ← hand-maintained execution order
32
32
  ├── backlog.md ← unordered, what is not being scheduled
33
33
  ├── session-<slug>.md ← optional, what a compaction is about to destroy
34
+ ├── archive/ ← shipped tasks, moved by canon tasks archive
35
+ ├── declined/ ← decided-against tasks, moved by canon tasks decline
34
36
  ├── v09.0-sync-paths.md # canon-allow-reference: illustrates the vXX.Y-slug filename this section defines
35
37
  └── v13.0-toolkit-drift.md # canon-allow-reference: illustrates the vXX.Y-slug filename this section defines
36
38
  ```
@@ -207,7 +209,7 @@ Every task names where it came from, through a `Plan:`, `Groundwork:`, `Intake:`
207
209
 
208
210
  A task with no origin is either lost context or work nobody decided to do. The invariant runs both ways, and the second direction is the one that bites: a groundwork track, an intake folder, or an open issue that no task points at is work already decided and on its way to being forgotten.
209
211
 
210
- An intake folder answers that direction at folder scope rather than item scope, since one dump dispositions many items and most close without ever becoming a task. What names a folder is every item answered and no task citing it, on the board or in the archive. That is a dump nobody acted on. Counting the archive beside the board is what separates it from one already promoted and shipped, and a check reading the board alone calls every finished folder abandoned.
212
+ An intake folder answers that direction at folder scope rather than item scope, since one dump dispositions many items and most close without ever becoming a task. What names a folder is every item answered and no task citing it, on the board or in the archive. That is a dump nobody acted on. Counting the archive and declined folder beside the board is what separates it from one already promoted and settled, and a check reading the board alone calls every finished folder abandoned.
211
213
 
212
214
  `Plan:`, `Groundwork:`, and `Intake:` name their target as a markdown link whose text is the file or folder stem, so the line resolves on a ctrl-click the way `priority.md` rows already do. Write the path relative to `.canon/tasks/`, which makes it `../plans/`, `../groundwork/`, and `../intake/`. A path written from the project root renders as a link and resolves to nothing in an editor rooted at the project. `Issue:` stays a bare `#NNN`, since an issue number is not a path and a full URL would write the remote into a gitignored file.
213
215
 
@@ -258,3 +260,15 @@ Archiving a task archives its plan alongside it, when the closing task is that p
258
260
  One act rather than two is what makes the pair safe. The merge is the event that settles a plan, and a `post-merge` hook reaching the archive with nobody watching cannot act on a warning, so a second call after it would be a second failure point leaving the task archived and the plan live.
259
261
 
260
262
  A task with an open outcome stays on the board, and so does its plan. Close it, or cut it from the task when the work is being abandoned, so what was dropped is recorded rather than inferred from an archived file. Cutting means striking the outcome's body: `- ~~<outcome>~~ <why>`. `archiveTask` reads a struck body as cut whatever its checkbox holds, so a task carrying only cut outcomes still archives and a mixed task carries both counts on its success record.
263
+
264
+ ## Declining
265
+
266
+ A task decided against moves to `.canon/tasks/declined/` rather than `.canon/tasks/archive/`. The two folders answer different questions: archive means the work shipped, declined means somebody decided against doing it. Neither reading fits a task that is merely unscheduled, which stays on `backlog.md` rather than moving anywhere, since nobody has decided against it and it may still rise when the board has room.
267
+
268
+ `canon tasks decline` carries no outcome-state gate. A task can be decided against at any outcome state, open outcomes included, which is what separates its refusal set from archive's: the two never share one, since a shared gate would let one archive a task that cannot yet ship or decline one that already has.
269
+
270
+ The decision is recorded on the task itself with a `Declined:` line, in the `Plan:`/`Pull request:` family: `Declined: <reason>, <who> on <YYYY-MM-DD>`. It anchors the same way `Pull request:` does, after the last origin line the task carries. The line is free prose after the colon, since it names no file to link.
271
+
272
+ Declining a task moves its plan alongside it the same way archiving does, when the declining task is that plan's last live citation. A plan several tasks share stays where it is, and a declined task's plan lands in `.canon/plans/archive/` indistinguishable from a shipped one by folder alone. The task file under `.canon/tasks/declined/` is what records which it was.
273
+
274
+ The move clears whichever of `priority.md` or `backlog.md` holds the task's row, since a decided-against task most often comes off the backlog before anyone plans it, but a row already promoted to the ordering file is cleared the same way archive clears it.
@@ -0,0 +1,103 @@
1
+ name: Deploy
2
+ # canon-no-seed: a stack-specific deploy workflow with no root counterpart by design, parameterized per project on sync rather than mirrored from one.
3
+
4
+ on:
5
+ push:
6
+ branches:
7
+ - main
8
+ paths:
9
+ # Fill in this project's own build inputs. A single glob covering the
10
+ # source tree is enough for most projects; widen it to cover every
11
+ # path the build reads when it draws on content living elsewhere.
12
+ - 'src/**'
13
+ workflow_dispatch:
14
+
15
+ # Fill in a concurrency group name unique to this project. A second push
16
+ # while a deploy is in flight publishes the newer commit rather than racing
17
+ # it, and the running job is left to finish so a half-uploaded site is never
18
+ # what a reader lands on.
19
+ concurrency:
20
+ group: deploy
21
+ cancel-in-progress: false
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ build:
28
+ name: 📦 Build
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Checkout
32
+ uses: actions/checkout@v4
33
+
34
+ - name: Setup Bun
35
+ uses: oven-sh/setup-bun@v2
36
+ with:
37
+ bun-version: latest
38
+
39
+ - name: Install Dependencies
40
+ run: bun install --frozen-lockfile
41
+
42
+ - name: Build
43
+ run: bun run build
44
+
45
+ - name: Upload artifact
46
+ uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist
50
+ retention-days: 1
51
+
52
+ deploy:
53
+ name: 🚀 Deploy
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - name: Check for Cloudflare secrets
58
+ id: secrets-check
59
+ env:
60
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
61
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
62
+ run: |
63
+ if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
64
+ echo "skip=true" >> "$GITHUB_OUTPUT"
65
+ else
66
+ echo "skip=false" >> "$GITHUB_OUTPUT"
67
+ fi
68
+
69
+ - name: Skip notice
70
+ if: steps.secrets-check.outputs.skip == 'true'
71
+ run: echo "::notice::CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID is not set, skipping deploy. Run the deploy-cloudflare skill to finish setup."
72
+
73
+ # This job builds nothing and still needs a toolchain. The checkout
74
+ # below brings bun.lock, wrangler-action reads it, picks bun on the
75
+ # strength of it, and installs wrangler with that, so a deploy with no
76
+ # bun on PATH fails before it reaches Cloudflare. Dropping these steps
77
+ # as dead weight breaks the deploy on every push.
78
+ - name: Checkout
79
+ if: steps.secrets-check.outputs.skip == 'false'
80
+ uses: actions/checkout@v4
81
+
82
+ - name: Setup Bun
83
+ if: steps.secrets-check.outputs.skip == 'false'
84
+ uses: oven-sh/setup-bun@v2
85
+ with:
86
+ bun-version: latest
87
+
88
+ - name: Download artifact
89
+ if: steps.secrets-check.outputs.skip == 'false'
90
+ uses: actions/download-artifact@v4
91
+ with:
92
+ name: dist
93
+ path: dist
94
+
95
+ - name: Publish
96
+ if: steps.secrets-check.outputs.skip == 'false'
97
+ uses: cloudflare/wrangler-action@v3
98
+ with:
99
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
100
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
101
+ # Fill in this project's Pages project name, set when the
102
+ # deploy-cloudflare skill runs wrangler pages project create.
103
+ command: pages deploy dist --project-name=REPLACE_WITH_PROJECT_NAME --branch=${{ github.ref_name }}
@@ -0,0 +1,5 @@
1
+ [stack]
2
+ name = "cloudflare"
3
+ extends = ""
4
+ runtime = "bun"
5
+ scaffold = ""
@@ -0,0 +1,24 @@
1
+ # Tooling cloudflare reference
2
+
3
+ ## Overview
4
+
5
+ The cloudflare stack ships one parameterized GitHub Actions workflow that deploys a static build to Cloudflare Pages. It is keyed on host rather than on framework and carries no dependencies, seeds, or scripts of its own, so it syncs onto a project already carrying `astro`, `vite-react`, or any other stack at the same root without colliding.
6
+
7
+ ## Deploy workflow
8
+
9
+ - Separate `build` and `deploy` jobs, connected by an uploaded artifact, converged on the shape already running in the closest prior art rather than invented fresh.
10
+ - The deploy job checks for `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` before doing anything else, and skips with a `::notice::` rather than failing when either is absent. A first merge lands ahead of the Pages project existing, and this guard is what keeps that push green.
11
+ - `--branch=${{ github.ref_name }}` on every deploy, which gives every branch a Cloudflare preview URL rather than only `main`.
12
+ - The deploy job checks out the repository and installs Bun even though it builds nothing. `wrangler-action` resolves its package manager from `bun.lock` in the checkout, and dropping that step breaks the deploy on every push once the toolchain it relies on silently goes missing.
13
+
14
+ ## Parameters a project fills in by hand
15
+
16
+ Three values in the seeded file have no generic default and are set once, right after sync:
17
+
18
+ - The `paths:` filter under the `push` trigger. The shipped default is a single `src/**` glob. Widen it to cover every path the build reads when the build draws on catalogs or generated content living outside `src/`.
19
+ - The `concurrency.group` name. The shipped default is the bare string `deploy`, which is enough for a single-deploy project. Give it a project-specific name only if the project runs more than one deploy workflow.
20
+ - The `--project-name` flag on the `wrangler pages deploy` command. Set it to the name the `deploy-cloudflare` skill creates the Pages project under.
21
+
22
+ ## Re-sync
23
+
24
+ `configs/` is golden and always overwrites. Re-running `canon tooling sync cloudflare . --write` after hand-editing any of the three parameters above discards that edit and restores the shipped defaults. Re-fill the parameters after any re-sync rather than expecting the file to preserve them.