@erclx/canon 4.105.0 → 4.106.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/draft-identity/SKILL.md +84 -9
- package/claude/skills/role-planner/REQUIREMENT.md +14 -7
- package/claude/skills/role-planner/SKILL.md +56 -53
- package/claude/skills/role-worker/SKILL.md +12 -4
- package/claude/skills/session-relay/REQUIREMENT.md +44 -17
- package/claude/skills/session-relay/SKILL.md +83 -14
- package/docs/agents/tasks.md +2 -0
- package/docs/workflow/ai-workflow.md +9 -9
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/src/claude/cases/workflow.ts +1 -1
- package/src/gate/measures.ts +28 -5
- package/src/tasks/archive.ts +77 -10
- package/tooling/astro/configs/card-src/pages/og-card.astro +98 -0
- package/tooling/astro/configs/card.config.mjs +39 -0
- package/tooling/astro/configs/scripts/check-card-exclusion.sh +58 -0
- package/tooling/astro/manifest.toml +3 -0
- package/tooling/astro/reference.md +27 -0
- package/tooling/web/configs/scripts/verify.sh +9 -0
- package/tooling/web/reference.md +4 -2
|
@@ -38,7 +38,24 @@ Announce which of the two decided the sequence.
|
|
|
38
38
|
|
|
39
39
|
## Step 3: detect the write folder
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
A root-only check finds a project's assets only where the project keeps them at the root. A repository holding its site in a subfolder keeps them a level down, and a Next project keeps its favicon under the App Router convention with no `public/` at all, so both fall through to the root fallback while the announcement reads as a detected folder. Walk one level down and know that convention rather than falling through.
|
|
42
|
+
|
|
43
|
+
Read both, from the project root:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
find . -maxdepth 3 \( -name node_modules -o -name .git -o -name dist -o -name .astro \) -prune -o -type d \( -name public -o -name static \) -print | sort
|
|
47
|
+
find . -maxdepth 4 \( -name node_modules -o -name .git \) -prune -o -type d -path '*/src/app' -print | sort
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Take the first tier that answers:
|
|
51
|
+
|
|
52
|
+
1. A root `public/` or `static/`.
|
|
53
|
+
2. A `public/` or `static/` one level down, such as `web/public/`. Where the first read returns more than one, take the shallowest, and the one whose parent holds the project's own framework config where two sit at the same depth.
|
|
54
|
+
3. A `src/app/` folder, at the root or one level down, which is the Next App Router convention and where that framework's own favicon and card files sit. Take the folder itself, not a `public/` beside it.
|
|
55
|
+
4. A stack-declared asset folder, where the project's own config names one.
|
|
56
|
+
5. The project root, which is the fallback rather than a detection.
|
|
57
|
+
|
|
58
|
+
Announce which tier decided, naming the tier rather than the path alone. On tier 5 say the detection found nothing and the root is a fallback, since a folder reported without that word reads as a folder the project declared. Announce it either way so the operator can move the files where the project's own convention differs.
|
|
42
59
|
|
|
43
60
|
## Step 4: name the decision and the arms
|
|
44
61
|
|
|
@@ -50,14 +67,12 @@ Structure every arm as a `.mark` element, the inline SVG alone, nested inside a
|
|
|
50
67
|
|
|
51
68
|
Follow `${CLAUDE_SKILL_DIR}/../draft-and-pick/SKILL.md` Steps 2 through 5 against the arms from Step 4: author the page, render and hand off, take the pick, and loop on it.
|
|
52
69
|
|
|
53
|
-
## Step 6: finalize
|
|
70
|
+
## Step 6: finalize the mark and the icons
|
|
54
71
|
|
|
55
|
-
This step
|
|
72
|
+
This step and Step 7 replace `draft-and-pick`'s own Step 6, since the pick here produces several final files rather than one applied surface.
|
|
56
73
|
|
|
57
74
|
1. Extract the picked arm's `.mark` markup as the final vector source. Write it to `<write-folder>/favicon.svg`.
|
|
58
|
-
2. Under `<dest>/render/`, write one page per Step 2 size
|
|
59
|
-
- `icon-<w>x<h>.html`: the picked `.mark` markup, its `.render` wrapper sized `<w>/2` by `<h>/2`
|
|
60
|
-
- `og-image.html`: the picked `.card` markup, its `.render` wrapper sized 600x315 to capture at the native 1200x630
|
|
75
|
+
2. Under `<dest>/render/`, write one `icon-<w>x<h>.html` page per Step 2 size, carrying the picked `.mark` markup. Give every page's captured element the shared class `.render`, so one capture call renders the whole batch regardless of the size spread. `canon capture` opens every page at a fixed 2x device scale factor and screenshots the element at that scale, so declare each `.render` element at half its target dimension, `<w>/2` by `<h>/2`, to land the captured PNG on the literal target size rather than double it. Declare a machine-resolved font stack (`system-ui` behind a generic fallback) on each, since `canon capture` refuses a page naming no font at all.
|
|
61
76
|
3. Render the batch:
|
|
62
77
|
|
|
63
78
|
```bash
|
|
@@ -65,16 +80,75 @@ This step replaces `draft-and-pick`'s own Step 6, since the pick here produces s
|
|
|
65
80
|
```
|
|
66
81
|
|
|
67
82
|
4. Report each written file's path and the dimensions `canon capture` printed for it, confirming each equals its Step 2 target rather than half of it.
|
|
68
|
-
|
|
83
|
+
|
|
84
|
+
The card leaves this batch and takes Step 7 instead. An icon is the mark alone at a fixed size and a scratch page renders it faithfully, where the card is a composition of the mark with the project's own type and color and a scratch page can only carry typed copies of both.
|
|
85
|
+
|
|
86
|
+
## Step 7: write the card as a route and capture it through the server
|
|
87
|
+
|
|
88
|
+
A card captured from a page this run deletes leaves the project a PNG and nothing that produced it, so renaming the project means another pass through the pick loop with an operator in it to move one string. Write the composition into a route the project keeps, and capture that route through a running server so it reads the project's real stylesheet, tokens and fonts.
|
|
89
|
+
|
|
90
|
+
This is Astro-shaped in v1, because Astro is the one web stack whose config gives a route a structural build exclusion. A project already holding the card route scaffold takes the route path, and everything else falls to the page path below. Say which one decided, since the two leave the project different things.
|
|
91
|
+
|
|
92
|
+
### The route path
|
|
93
|
+
|
|
94
|
+
1. Check that the project already holds the scaffold, being a `card.config.mjs` with a `card-src/pages/og-card.astro` beside it. Take the page path below where either is missing, and leave the install to the operator:
|
|
95
|
+
|
|
96
|
+
```plaintext
|
|
97
|
+
No card route installed, so the card shipped as a PNG this run.
|
|
98
|
+
`canon tooling sync astro . --write` installs the route, and it replaces every other
|
|
99
|
+
golden config in the astro stack, so run it when you mean to sync the stack.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Never run that sync to obtain the route. It copies the whole stack over the project's own framework, lint, typescript, and test configs, which is a cost nobody asking for a logo agreed to. Hand-writing the scaffold instead is the other wrong answer, since a copy written here drifts from the one the stack maintains.
|
|
103
|
+
|
|
104
|
+
The scaffold is a second framework config pointing `srcDir` at a folder beside `src/`, the route inside that folder importing the project's global stylesheet and carrying a fixed `<meta>` marker, and a shell check failing a build that carries the marker. Of the route's three consumers, the published build is the only one a check reaches. Read the shape with `canon tooling reference astro`.
|
|
105
|
+
|
|
106
|
+
2. Write the picked arm's `.card` markup into the route's `.card` element, leaving the marker, the stylesheet import and the surrounding document alone. Keep the element at 600x315, half of the 1200x630 target, since the capture's 2x scale factor is fixed and a 1200x630 element captures at 2400x1260.
|
|
107
|
+
|
|
108
|
+
3. Start the card server in the background and read the port off its own output:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bun run card:dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It serves under a strict port, so it refuses rather than walking to the next free one. Set `WORKTREE_PORT_OFFSET` by hand and start it again where something already holds that port.
|
|
115
|
+
|
|
116
|
+
4. Capture the route to the write folder, naming the destination PNG rather than a directory, which is what a URL source takes for `--out`:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
canon capture http://localhost:<port>/og-card --selector .card --out <write-folder>/og-image.png
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
5. Stop the server. Report the dimensions `canon capture` printed, confirming 1200x630.
|
|
123
|
+
|
|
124
|
+
A refusal naming a font here is the mechanism working rather than a failure to route around. It says the route asked for a family the machine cannot resolve, which is the same rewrap a published card would have shipped silently.
|
|
125
|
+
|
|
126
|
+
### The page path
|
|
127
|
+
|
|
128
|
+
Where the project holds no route to write into, write `og-image.html` into `<dest>/render/` with its `.render` wrapper sized 600x315, then capture that one page:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
canon capture <dest>/render/og-image.html --selector .render --out <write-folder>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
It takes its own call rather than riding Step 6's batch, since Step 6 runs before this step chooses a path and the batch is already captured by the time the choice is made. Say that the card shipped as a PNG with no source, and that the project gains one when it grows a route to hold the composition.
|
|
135
|
+
|
|
136
|
+
### Closing the scratch folder
|
|
137
|
+
|
|
138
|
+
Delete `<dest>` per `draft-and-pick`'s own scratch-folder rule. The route is not a leftover variant that rule forbids: a variant is one of several candidates none of which was chosen, and the route holds the arm the operator picked. The losing arms are what that rule is about, and they are already archived and deleted by the step it belongs to.
|
|
139
|
+
|
|
140
|
+
A project whose card was generated before this route existed keeps its PNG. Say so rather than regenerating it, since re-running the pick loop spends an operator's attention on a file nobody complained about, and the next real edit to the card picks up the route.
|
|
69
141
|
|
|
70
142
|
## Response format
|
|
71
143
|
|
|
72
144
|
```plaintext
|
|
73
145
|
📝 Wrote <write-folder>/favicon.svg
|
|
74
146
|
📝 Wrote <write-folder>/icon-16x16.png (16x16)
|
|
147
|
+
📝 Wrote card-src/pages/og-card.astro (route path only)
|
|
75
148
|
📝 Wrote <write-folder>/og-image.png (1200x630)
|
|
76
149
|
|
|
77
|
-
Write folder: <
|
|
150
|
+
Write folder: <tier that decided>. Move the files if this project's own convention differs.
|
|
151
|
+
Card source: <the route, captured through the card server|a scratch page, since this project holds no card route>.
|
|
78
152
|
```
|
|
79
153
|
|
|
80
154
|
## What this delegates
|
|
@@ -83,4 +157,5 @@ Cite these rather than restating them.
|
|
|
83
157
|
|
|
84
158
|
- `draft-and-pick` owns Steps 1 through 5 of the render-and-pick loop, cited above
|
|
85
159
|
- `design-extract` owns building `canon/DESIGN.md`. This skill only reads it.
|
|
86
|
-
- `canon capture` owns the render mechanics, its font refusal, and its reported dimensions
|
|
160
|
+
- `canon capture` owns the render mechanics, its font refusal, its fixed 2x scale factor, and its reported dimensions. It already reads an `http(s)://` source, where `--out` names the destination PNG rather than a directory, so the route capture needs nothing added to it.
|
|
161
|
+
- The `astro` tooling stack owns the card route's scaffold, its second config, and the exclusion check, read with `canon tooling reference astro`. This skill writes into the route and never reshapes it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: role-planner
|
|
3
|
-
description: What a
|
|
3
|
+
description: What a thinking session is across planning, groundwork, and intake, how it reads what is in flight, the writes-records-never-source boundary, and what it hands back to whoever dispatched it
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Role planner requirement
|
|
@@ -49,25 +49,30 @@ four carried at least one, caught only because the planner ran
|
|
|
49
49
|
|
|
50
50
|
## Must
|
|
51
51
|
|
|
52
|
-
- Assert what a
|
|
52
|
+
- Assert what a thinking session is, what it may not write, and how long the role lasts, since `plan-feature`, `plan-groundwork`, and `plan-intake` each carry their own procedure and no body carries the role the three share
|
|
53
|
+
- Cover all three of those skills rather than `plan-feature` alone, since a groundwork or intake session took no role at all while the body was written for one plan under one task
|
|
54
|
+
- State the writes-records-never-source boundary once, and name the three folders in one line while deferring each scope's exceptions to the skill that owns it, so a change to one scope edits one body
|
|
55
|
+
- Carry `plan-groundwork`'s close-time task file as an exception to the board ban, since widening the role without it makes the role forbid what that skill requires
|
|
53
56
|
- State the in-flight read as a command composing the session roster with open pull requests, and say that a branch and a worktree are not evidence, since a count of either reported merged work as live
|
|
54
57
|
- Run that read once per task rather than once per batch, since a reused session ages its picture of the tree while it works
|
|
55
58
|
- Name every read a plan needed and a launch string did not carry, the task file's findings and the source files among them, since a count quoted from a row was wrong or stale in ten places across four plans
|
|
56
59
|
- Name the validate and audit calls the written plan passes through, since nothing else opens a plan and the audit caught ban hits in three of four trial plans
|
|
57
60
|
- State the shared board as read-only and name the cross-feature call as the controller's, since a planner reading blockers and file sets can write a merge order off a partial picture and will not notice that it has
|
|
58
|
-
- Owe an announcement when the
|
|
61
|
+
- Owe an announcement when the record lands, carrying the path and what the row got wrong, since the controller cannot watch the read happen
|
|
59
62
|
- Owe a message before a block becomes an interactive prompt, since a session already waiting on input never reaches the tool round an inbound message drains at
|
|
60
63
|
- Keep correcting the dispatcher a first-class move carrying its evidence, since a trial corrected a brief's premise, planned every row anyway, and carried the consequence into a plan question
|
|
61
|
-
- Point at
|
|
64
|
+
- Point at each of the three skills for its record's shape and the steps that write it
|
|
65
|
+
- Defer the addressee resolution to `session-relay` and keep only the last-rung inference, which discriminates differently here than it does for a worker because a session in this role holds no feature branch either
|
|
62
66
|
|
|
63
67
|
## Must not
|
|
64
68
|
|
|
65
|
-
- Restate
|
|
69
|
+
- Restate a record's sections, its suggested-and-answer contract, or the steps the running skill carries, since thinness is what keeps one body correct for a dispatched session reading it as its whole contract
|
|
70
|
+
- Restate the write-scope exceptions each of the three skills states about its own folder
|
|
66
71
|
- Restate a boundary `role-orchestrator` or `role-worker` states about itself
|
|
67
72
|
- Tell a planner to halt on a plan question, which the suggested line already answers, rather than on what blocks writing the plan at all
|
|
68
73
|
- Tell a planner to report the path with no account of what the row got wrong. That instruction came from a trial condition holding a blind comparison intact, and it is not a durable obligation.
|
|
69
74
|
- Report progress through the channel, which rebuilds on the sender's side the poll the announcement exists to retire
|
|
70
|
-
- Write the priority board
|
|
75
|
+
- Write the priority board or the backlog, at any size, or the task file outside `plan-groundwork`'s close-time exception
|
|
71
76
|
- Be a skill nothing invokes but its author typing the name. `orchestrator-dispatch.md` names it on the planning launch the way it names `role-worker` on a build, so a stretch where only a typed invocation reaches it is the signal that the role never took.
|
|
72
77
|
|
|
73
78
|
## Guards
|
|
@@ -79,7 +84,9 @@ four carried at least one, caught only because the planner ran
|
|
|
79
84
|
|
|
80
85
|
## Out of scope
|
|
81
86
|
|
|
82
|
-
-
|
|
87
|
+
- Each record's sections and its answer contract, which the standard behind it fixes, and the steps that write the file, which `plan-feature`, `plan-groundwork`, and `plan-intake` each own for their own
|
|
88
|
+
- The write-scope exceptions, which stay in the three skills: groundwork's close-time task file, its fixture and evidence paths, and intake's statement that it has none
|
|
89
|
+
- The mechanical addressee resolution, which `session-relay` owns for every session with or without a role
|
|
83
90
|
- The branch, the build, and the pull request, which `role-worker` and `auto-ship` own
|
|
84
91
|
- Deciding which rows run and in what order, which is the controlling session's call and stated in `role-orchestrator`
|
|
85
92
|
- The dispatch itself, its collision check, and its disjointness gate, which `orchestrator-dispatch.md` holds
|
|
@@ -1,54 +1,71 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: role-planner
|
|
3
|
-
description: Asserts the planner role for a session writing one plan under one
|
|
3
|
+
description: Asserts the planner role for a session writing one plan, groundwork track, or intake pass under one row, holding what it reads before deciding, what may be written and where, how to read what is already in flight, and what it hands back. Use when asked to "be the planner", "you are a planner session", at the start of a dispatched or hand-launched planning, groundwork, or intake run, or when a planning session needs to know what it may not write. Do NOT use to write the record itself, which is `plan-feature`, `plan-groundwork`, or `plan-intake`, to make the cross-feature merge call, or to implement.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Role planner
|
|
7
7
|
|
|
8
|
-
This session
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
This session thinks rather than builds. It reads the row, measures what the row
|
|
9
|
+
claims against the tree, writes one record, and hands back the path. The record
|
|
10
|
+
is a feature plan under `plan-feature`, a groundwork track under
|
|
11
|
+
`plan-groundwork`, or an intake pass under `plan-intake`, and the role is the
|
|
12
|
+
same across all three.
|
|
11
13
|
|
|
12
14
|
It does not implement, it does not decide what merges before what, and it does
|
|
13
15
|
not review. Those belong to a worker, to the controlling session, and to the
|
|
14
16
|
human.
|
|
15
17
|
|
|
16
18
|
This body states the role, the reads, the boundaries, and the channel, and it
|
|
17
|
-
starts no step of its own.
|
|
18
|
-
and cites the standard fixing
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
starts no step of its own. Each of the three skills owns the steps that write
|
|
20
|
+
its record and cites the standard fixing that record's shape, so read the
|
|
21
|
+
procedure there and reach it from the launch rather than from here.
|
|
22
|
+
|
|
23
|
+
"Planner" stretches to cover measuring and triage. The operator accepted that,
|
|
24
|
+
since what the three share is the boundary below rather than the artifact each
|
|
25
|
+
produces.
|
|
21
26
|
|
|
22
27
|
## Where the session stands
|
|
23
28
|
|
|
24
|
-
- Write
|
|
25
|
-
-
|
|
26
|
-
- Resolve
|
|
27
|
-
-
|
|
29
|
+
- Write records, never source. A tracked file under `src/`, a standard, a rule, a reference doc, and a seed all sit outside what this session may touch, whichever of the three skills it is running.
|
|
30
|
+
- Write only into the folder the running skill names: `plan-feature` at `.canon/plans/`, `plan-groundwork` at `.canon/groundwork/<nn>-<slug>/`, and `plan-intake` at `.canon/intake/<nn>-<slug>/`. Each skill's own `## Write scope` carries its exceptions, which is why they are not listed here. A change to one scope then edits one body.
|
|
31
|
+
- Resolve every one of those folders, and `.canon/tasks/` and `.canon/review/` beside them, at the main worktree root. They are gitignored, so a copy beside a linked worktree is absent rather than empty and reads as a folder nobody has written yet.
|
|
32
|
+
- Never enter a worktree, never create a branch, and never write a tracked file. A thinking session that builds has stopped being one, and the row loses the independent read the worker's own session was going to bring to it.
|
|
33
|
+
- This session holds no worktree, so send each record out as a plain `Bash` heredoc past the same main-root refusal a linked worktree meets. That route skips the write-matched hooks, which are no-ops on a record file.
|
|
34
|
+
- Work the row the launch named. Report a second row met while reading rather than taking it on, since which rows run is the controller's call.
|
|
28
35
|
|
|
29
36
|
## What to read before deciding
|
|
30
37
|
|
|
31
|
-
Each item below is something a
|
|
38
|
+
Each item below is something a record needed and a launch string did not carry.
|
|
32
39
|
|
|
33
40
|
- The task file, and its `## Findings` before deciding anything. A row can carry its own disproof under a title that still states the original claim, which is how one trial nearly planned against a premise the file had already recorded as dead.
|
|
34
41
|
- The source files themselves, opened rather than summarized. Never a count quoted from the task file, which was wrong or stale in ten places across four plans.
|
|
35
42
|
- `CLAUDE.md` and `canon/ARCHITECTURE.md`, for the decision and the alternative it was taken against rather than for the decision alone.
|
|
36
|
-
- The plan of any row this one is sequenced behind, including one already moved to `.canon/plans/archive/`. Reading a shipped plan produced the strongest constraint in the first trial and no brief asked for it.
|
|
37
43
|
- `.canon/tasks/priority.md` for the row's Touches column and its stated blocker. That column is the file set the dispatch disjointness gate already reads.
|
|
44
|
+
|
|
45
|
+
Two reads belong to the `plan-feature` path alone, since neither a groundwork
|
|
46
|
+
track nor an intake pass is sequenced behind another row or validated as a plan:
|
|
47
|
+
|
|
48
|
+
- The plan of any row this one is sequenced behind, including one already moved to `.canon/plans/archive/`. Reading a shipped plan produced the strongest constraint in the first trial and no brief asked for it.
|
|
38
49
|
- `${CLAUDE_SKILL_DIR}/../../standards/plan.md` for the shape, then `canon records validate plans` and `canon markdown audit <the plan file>` on the file once it is written. Nothing else opens a plan, since `.canon/plans/` is gitignored and the audit's default path set is what git lists.
|
|
39
50
|
|
|
51
|
+
A groundwork or intake record passes through whatever its own skill and standard
|
|
52
|
+
name instead. The gitignored-folder problem is the same for all three, so run
|
|
53
|
+
the audit on any record a session writes rather than trusting a gate to reach
|
|
54
|
+
it.
|
|
55
|
+
|
|
40
56
|
## Read what is in flight rather than inferring it
|
|
41
57
|
|
|
42
58
|
- Run `canon sessions list --json`, take the `branch` field of every session whose `repository` matches this one and whose branch is neither null nor the trunk branch, union that with every `headRefName` from `gh pr list --json number,headRefName`, dedupe by branch name, then diff each with `git diff --name-only main...<branch>` for its file set. That resolves locally whether or not a pull request exists, since every track in this repository shares one git directory across its worktrees.
|
|
43
59
|
- A bare branch or worktree with neither a live session nor a pull request behind it is still not evidence, since this repository squash-merges and leaves both behind. The roster read is already filtered to live sessions, so what changed is the second source composed with it rather than a raw count. It still misses a live session that reached its worktree through the direct-path fallback rather than `EnterWorktree`, since that session's registered branch never moves off the trunk.
|
|
44
|
-
- Run the read once per
|
|
45
|
-
- Name each in-flight set as a constraint, say which act it forbids, and stamp the block with the commit the tree was read at
|
|
60
|
+
- Run the read once per row rather than once per batch. A session working several rows ages its picture of the tree while it goes, and this read is what dates it.
|
|
61
|
+
- Name each in-flight set as a constraint, say which act it forbids, and stamp the block with the commit the tree was read at. A bare path list leaves the reader guessing. The plan standard fixes that block for a feature plan, and a groundwork or intake record carries the same three parts in whatever shape its own standard states.
|
|
46
62
|
|
|
47
63
|
## The board is read-only
|
|
48
64
|
|
|
49
65
|
- Never write `.canon/tasks/priority.md` or `.canon/tasks/backlog.md`. Both are gitignored, so an overwrite drops a row with no history to recover it from.
|
|
50
|
-
- Never write the task file
|
|
51
|
-
-
|
|
66
|
+
- Never write the task file. The record is the whole output, and a row edited from here changes what the controller reads back as the state of the board.
|
|
67
|
+
- One exception, and it belongs to `plan-groundwork` alone: that skill writes a single task file at close, recording what the track concluded. Its own `## Write scope` states the exception, and this line exists so the rule above does not forbid what that skill requires.
|
|
68
|
+
- Report what the row got wrong rather than repairing it. A stale count, a moved line, or a path that no longer resolves goes into the record and into the handback, and the controller decides which of the two carries the correction.
|
|
52
69
|
- Leave the cross-feature call alone. Which rows collide, what merges before what, and whether a row should run at all stay with the controller. `priority.md` shows blockers and file sets and is enough to write a confident merge order off a partial picture, which is the failure this boundary exists to prevent.
|
|
53
70
|
|
|
54
71
|
## The channel
|
|
@@ -56,47 +73,33 @@ Each item below is something a plan needed and a launch string did not carry.
|
|
|
56
73
|
The controlling session cannot watch this one read, so two messages are owed and
|
|
57
74
|
nothing else.
|
|
58
75
|
|
|
59
|
-
- Announce the
|
|
60
|
-
- Send a block out as a message before it becomes an interactive prompt.
|
|
61
|
-
- Send nothing on progress. A
|
|
62
|
-
|
|
63
|
-
Address the session the launch named
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
returns a sibling or this session itself.
|
|
78
|
-
|
|
79
|
-
Inferring is the last rung and it discriminates less here than it does for a
|
|
80
|
-
worker. That read takes the sessions holding no feature branch, which separates a
|
|
81
|
-
controller from a worker because a worker holds one, and a planner holds none
|
|
82
|
-
either, so every sibling planner comes back beside the controller. Report the
|
|
83
|
-
candidates and stop where more than one does, rather than addressing the first.
|
|
84
|
-
Send to the single row where exactly one comes back, and say the addressee was
|
|
85
|
-
inferred.
|
|
86
|
-
|
|
87
|
-
Compose the relay through `canon:session-relay` when this section finds no
|
|
88
|
-
message-sending tool to send through, rather than leaving a resolved addressee
|
|
89
|
-
with nothing to reach it. It runs the same ladder above and carries the
|
|
90
|
-
message already owed, so nothing here is stated twice.
|
|
76
|
+
- Announce the record as the file lands, carrying its path and what the task file got wrong. Write no summary beside it, which is a second account of a document the reader is about to open.
|
|
77
|
+
- Send a block out as a message before it becomes an interactive prompt.
|
|
78
|
+
- Send nothing on progress. A thinking session reporting progress rebuilds, on this side of the channel, the poll the announcement retires on the other.
|
|
79
|
+
|
|
80
|
+
Address the session the launch named, which it names as a `sessionId` rather
|
|
81
|
+
than a name. `canon:session-relay` turns that id into an address and carries the
|
|
82
|
+
send, so invoke it rather than resolving a name here. It reads the roster at the
|
|
83
|
+
moment of sending, checks the result against the agent listing, and falls back
|
|
84
|
+
to a copyable block where this session holds no tool to send through.
|
|
85
|
+
|
|
86
|
+
One rung stays here, because it is the rung that differs between roles.
|
|
87
|
+
Inferring an addressee discriminates less for a thinking session than for a
|
|
88
|
+
worker. The inference takes the sessions holding no feature branch, which
|
|
89
|
+
separates a controller from a worker because a worker holds one, and a session
|
|
90
|
+
in this role holds none either, so every sibling comes back beside the
|
|
91
|
+
controller. Report the candidates and stop where more than one does, rather than
|
|
92
|
+
addressing the first. Send to the single row where exactly one comes back, and
|
|
93
|
+
say the addressee was inferred.
|
|
91
94
|
|
|
92
95
|
## Refusing is part of the job
|
|
93
96
|
|
|
94
97
|
- Refuse an instruction the tree contradicts, and carry the evidence with it. Name the commands read and what complying would produce, rather than reporting reluctance.
|
|
95
98
|
- Correct a premise the brief carried when the tree disagrees with it. One trial was told two rows were free of a renamed token and found they carried it as command names rather than path segments, planned all three rows anyway, and put the consequence into a plan question. Correct, continue, and record what the correction changed.
|
|
96
|
-
- Answer a plan question rather than halting on it. Every question carries a `- Suggested:` line the operator accepts or overrules, so a judgment written down is the deliverable. Halt only on what blocks writing the
|
|
99
|
+
- Answer a plan question rather than halting on it. Every question carries a `- Suggested:` line the operator accepts or overrules, so a judgment written down is the deliverable. Halt only on what blocks writing the record at all, and send that out as a message before it becomes a prompt.
|
|
97
100
|
|
|
98
101
|
## Lifetime
|
|
99
102
|
|
|
100
|
-
- The role runs from the launch to the
|
|
101
|
-
- A reused session pays the context load once and stales differently. Its picture of the tree ages while it works, which is why the in-flight read runs per
|
|
103
|
+
- The role runs from the launch to the record landing, and one record closes it. A session handed a second row works it under the same role and reads the tree again for that row.
|
|
104
|
+
- A reused session pays the context load once and stales differently. Its picture of the tree ages while it works, which is why the in-flight read runs per row, and it accumulates toward a compaction that drops the reasoning behind its earlier records with nothing reporting it. Cap a reused session rather than letting it run the board.
|
|
102
105
|
- Treat this body as possibly older than the branch under it. A plugin skill loads from the marketplace cache rather than from the working tree.
|
|
@@ -89,10 +89,18 @@ with no roster row at all. Those nine were driving through Remote Control, which
|
|
|
89
89
|
writes no local process record, so a controller working from a phone is exactly
|
|
90
90
|
the case this read answers nothing for.
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
Send through `canon:session-relay`. It holds the mechanical half of the send,
|
|
93
|
+
turning the `sessionId` the launch named into an address at the moment of
|
|
94
|
+
sending, checking the result against the agent listing, and falling back to a
|
|
95
|
+
copyable block where this session holds no tool to send through. The message
|
|
96
|
+
owed stays in its own bullet above.
|
|
97
|
+
|
|
98
|
+
The rungs above restate the resolution the relay also carries. What is a
|
|
99
|
+
worker's own is the last of them, the inference over sessions holding no feature
|
|
100
|
+
branch, which discriminates here because a worker holds one and a controller
|
|
101
|
+
does not. The rest is duplicated deliberately, so this body still reads whole
|
|
102
|
+
for a session that reaches it with the relay unresolved, which is the ordinary
|
|
103
|
+
case in a target that installed governance without the plugin.
|
|
96
104
|
|
|
97
105
|
## Refusing is part of the job
|
|
98
106
|
|
|
@@ -1,43 +1,70 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: session-relay
|
|
3
|
-
description: Why a standalone skill with no firing condition never fired, why the first inline draft repeated the same protocol in two bodies,
|
|
3
|
+
description: Why a standalone skill with no firing condition never fired, why the first inline draft repeated the same protocol in two bodies, why the missing sender identity sat unaddressed through both attempts, and why the two guards that scoped it to a session holding a role and no send tool came off
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Session relay requirement
|
|
7
7
|
|
|
8
8
|
## Gap
|
|
9
9
|
|
|
10
|
-
Without this skill,
|
|
10
|
+
Without this skill, every session that owes another session a message resolves
|
|
11
|
+
the addressee itself. `role-worker` and `role-planner` each state the messages
|
|
12
|
+
owed and each carried the resolution inline, and a session holding neither role
|
|
13
|
+
carried it nowhere, so the mechanical half of the send existed in two copies
|
|
14
|
+
plus however many a roleless caller improvised.
|
|
11
15
|
|
|
12
|
-
A standalone skill was proposed for the gap and declined the same day. It fired
|
|
16
|
+
A standalone skill was proposed for the gap and declined the same day. It fired
|
|
17
|
+
on no condition of its own, since nothing routes a session to a skill matching
|
|
18
|
+
no request and reaching for no artifact, so a skill built to close the gap never
|
|
19
|
+
closed it. That history is why the pointer inside each role's
|
|
20
|
+
`## The channel` section still matters: it is a section both bodies read at
|
|
21
|
+
session start rather than one reached by request match alone, and it is what
|
|
22
|
+
makes the firing survive.
|
|
13
23
|
|
|
14
|
-
The first draft of this plan closed the gap with a paragraph drafted inline,
|
|
24
|
+
The first draft of this plan closed the gap with a paragraph drafted inline,
|
|
25
|
+
once inside `role-worker` and once inside `role-planner`. The operator overrode
|
|
26
|
+
that call: two bodies carrying identical protocol text is the shared-surface
|
|
27
|
+
case `canon/ARCHITECTURE.md` already decided against duplicating, since a later
|
|
28
|
+
fix reaching one copy and not the other diverges silently.
|
|
15
29
|
|
|
16
|
-
Neither attempt named who the relay is from. A message an operator relays by
|
|
30
|
+
Neither attempt named who the relay is from. A message an operator relays by
|
|
31
|
+
hand needs a sender as much as an addressee, and the standalone draft and the
|
|
32
|
+
inline draft both composed a body and an addressee with no line stating whose
|
|
33
|
+
turn produced it.
|
|
34
|
+
|
|
35
|
+
Scoping the skill to a session holding a role and no send tool left two holes
|
|
36
|
+
rather than one. A session holding neither role had no `## The channel` section
|
|
37
|
+
to carry the pointer and was refused outright on arrival. A session that held a
|
|
38
|
+
role and a send tool was refused too, which left the resolution ladder
|
|
39
|
+
duplicated in both role bodies for the ordinary case. Both guards came off
|
|
40
|
+
together, since the mechanics are the same whichever route carries the text.
|
|
17
41
|
|
|
18
42
|
## Must
|
|
19
43
|
|
|
20
|
-
- Fire
|
|
21
|
-
- Refuse a caller holding neither role, checked ahead of the tool-availability guard
|
|
44
|
+
- Fire for any session sending to another session, with or without a role and with or without a message-sending tool
|
|
22
45
|
- Read the sender's own name off `canon sessions list --self --json` before composing anything, and degrade to naming itself from the environment when the installed CLI answers `--self` with an unknown option rather than a refusal carrying a reason
|
|
23
|
-
- Resolve the addressee
|
|
24
|
-
-
|
|
46
|
+
- Resolve the addressee from a `sessionId` or a branch at the moment of sending, never by a name prefix
|
|
47
|
+
- Check the resolved name against the agent listing, sending it bare under a single row and completing it with the `[ref]` under more than one
|
|
48
|
+
- Send through a message-sending tool where one exists, and compose the copyable block where none does or where the listing carries no row to address
|
|
49
|
+
- State block-before-prompt, since the shipped channel rule carrying it for a session holding neither role reaches only a target that installed governance
|
|
50
|
+
- Carry the message body verbatim from whatever names it
|
|
25
51
|
- Open the composed text with a header naming the sender, the addressee, and what the message is
|
|
26
52
|
- State the header as a default a session may write around, rather than a fixed vocabulary
|
|
27
53
|
|
|
28
54
|
## Must not
|
|
29
55
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
56
|
+
- Draft a message body of its own. The text it composes belongs to the calling role's bullet or to the dispatch.
|
|
57
|
+
- Decide who the message goes to. The caller names an id or a branch and this body turns it into an address.
|
|
58
|
+
- Restate the last-rung inference `role-worker` and `role-planner` each carry, which differs between them
|
|
33
59
|
|
|
34
60
|
## Guards
|
|
35
61
|
|
|
36
|
-
- The
|
|
37
|
-
-
|
|
62
|
+
- The roster cannot name the sender: report the refusal and its reason in place of a name
|
|
63
|
+
- The resolution returns no row: report it rather than guessing, since the roster and the send channel enumerate different populations in both directions
|
|
64
|
+
- The caller named no addressee at all: defer to the calling role's own last rung, and ask the operator where one is present
|
|
38
65
|
|
|
39
66
|
## Out of scope
|
|
40
67
|
|
|
41
|
-
-
|
|
42
|
-
- The
|
|
43
|
-
- Inbound reach
|
|
68
|
+
- Which messages are owed and when, which stays in the calling role's own bullet or in the dispatch
|
|
69
|
+
- The last-rung inference, which stays in `role-worker` and `role-planner` because a worker holds a feature branch and a planner does not, so the two discriminate differently
|
|
70
|
+
- Inbound reach as a distinct case. A relay in either direction is one session sending to another, which is what this body now covers.
|
|
@@ -1,27 +1,92 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: session-relay
|
|
3
|
-
description:
|
|
3
|
+
description: Relays a message to another session, turning the sessionId the caller named into an address at the moment of sending and composing a copyable block where no send tool exists. Use when asked to "relay this", "message the controller", "send this over to the session that dispatched me", or "tell the other session", with or without a role. Do NOT use to decide what the message says, and do NOT use to assert what a session may write or is on the hook for, which is `role-worker` or `role-planner`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Session relay
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
This is the one place a session sends to another session. It holds the
|
|
9
|
+
mechanical half of the send: naming the sender, turning a `sessionId` or a
|
|
10
|
+
branch into an address that reaches, and putting the text through whichever
|
|
11
|
+
route exists. It holds none of the other half. What the message says and who it
|
|
12
|
+
goes to come from whatever dispatched this session, and from the calling role's
|
|
13
|
+
own `## The channel` section where the session holds a role.
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
It fires for a session holding a role and for one holding none. A roleless
|
|
16
|
+
session reaches this body directly and finds everything it owes stated here,
|
|
17
|
+
rather than reading a ladder written for a role it does not have.
|
|
11
18
|
|
|
12
|
-
##
|
|
19
|
+
## Step 1: name the sender
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
Read this session's own name from `canon sessions list --self --json`. A
|
|
22
|
+
message an operator relays by hand needs a sender as much as an addressee, and
|
|
23
|
+
a message arriving through a tool is read against whoever sent it.
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
A refusal carrying `no-self-identity` or `no-self-row` means the roster holds
|
|
26
|
+
nothing to name the sender with. Report the refusal and its reason in place of a
|
|
27
|
+
name rather than guessing one.
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
Where the installed CLI answers `--self` with an unknown option, that flag is
|
|
30
|
+
newer than the release this session holds, so there is no refusal and no reason
|
|
31
|
+
to report. Name the sender from what the environment states instead, and say the
|
|
32
|
+
roster did not answer, so the reader knows the name was not confirmed against
|
|
33
|
+
it.
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
## Step 2: resolve the addressee
|
|
36
|
+
|
|
37
|
+
The caller names the addressee as a `sessionId` or as a branch, never as a name.
|
|
38
|
+
A name is derived from what a session turns out to be doing and goes stale
|
|
39
|
+
inside the window a build or a plan takes, so resolve it at the moment of
|
|
40
|
+
sending rather than at launch.
|
|
41
|
+
|
|
42
|
+
- From a `sessionId`: run `canon sessions list --json`, find the row carrying that id, and take the `name` on it.
|
|
43
|
+
- From a branch: run the same read and take the row whose `branch` matches. Nothing renames a session when it takes a role, which is why an id is the stronger key and a branch is the fallback rather than the default.
|
|
44
|
+
|
|
45
|
+
Then check that name against the agent listing before sending it. A name is not
|
|
46
|
+
unique, and the roster carries no field separating two live sessions holding
|
|
47
|
+
one, so the resolution can end on a string that reaches the wrong session.
|
|
48
|
+
|
|
49
|
+
- One row under the name: send it bare.
|
|
50
|
+
- More than one row: complete the address with the `[ref]` that listing prints beside each row, rather than sending to the name alone, which lands on whichever row the channel resolves first. Two sessions differing only by a trailing ` (3)` is the shape this meets in practice.
|
|
51
|
+
|
|
52
|
+
Never filter that roster by a name prefix. Every self-dispatched worker is named
|
|
53
|
+
`worker-<project>-<slug>`, so a prefix scan returns a sibling or this session
|
|
54
|
+
itself, which is the defect that sent messages owed to a controller somewhere
|
|
55
|
+
else.
|
|
56
|
+
|
|
57
|
+
Report a resolution that returns nothing rather than falling back to a guess.
|
|
58
|
+
The roster and the send channel enumerate different populations in both
|
|
59
|
+
directions, measured at one moment: a live background session sat on the roster
|
|
60
|
+
that the agent listing did not carry, and nine sessions were addressable there
|
|
61
|
+
with no roster row at all. Those nine were driving through Remote Control, which
|
|
62
|
+
writes no local process record.
|
|
63
|
+
|
|
64
|
+
Where the caller named nobody at all, the calling role's own `## The channel`
|
|
65
|
+
section carries the last rung, since a worker and a planner infer differently
|
|
66
|
+
and a roleless caller has no ladder to defer to. Ask the operator where one is
|
|
67
|
+
present, put the candidate rows through the structured question surface so they
|
|
68
|
+
pick a row rather than recall a name, and say the addressee was inferred
|
|
69
|
+
wherever an inference decided it.
|
|
70
|
+
|
|
71
|
+
## Step 3: send
|
|
72
|
+
|
|
73
|
+
- A message-sending tool exists: send through it, addressed to what Step 2 resolved. This is the ordinary route.
|
|
74
|
+
- No message-sending tool, or Step 2 found no row to address: compose the block below and close the turn with nothing after it, so the block is the whole of what an operator copies.
|
|
75
|
+
|
|
76
|
+
Send the block out as a message before it becomes an interactive prompt. A
|
|
77
|
+
session already waiting on input never reaches the tool round that drains an
|
|
78
|
+
inbound message, so an answer relayed afterwards arrives under the open question
|
|
79
|
+
and changes nothing. This holds on both routes.
|
|
80
|
+
|
|
81
|
+
The shipped channel rule states the same thing for a session holding neither
|
|
82
|
+
role, so the restatement here is for the target that installed the plugin and
|
|
83
|
+
not governance, where that rule never arrives. A session that does load it reads
|
|
84
|
+
the rule and this body in agreement rather than one of them alone.
|
|
85
|
+
|
|
86
|
+
Carry the message body verbatim from whatever names it. This skill composes and
|
|
87
|
+
never drafts, so a pull request announcement, an address-review announcement, or
|
|
88
|
+
a blocked question is exactly the text the calling bullet or the dispatch
|
|
89
|
+
states, with its own facts filled in.
|
|
25
90
|
|
|
26
91
|
## Header
|
|
27
92
|
|
|
@@ -30,8 +95,12 @@ Relay: from <sender> to <addressee>, <what the message is>
|
|
|
30
95
|
<message body>
|
|
31
96
|
```
|
|
32
97
|
|
|
33
|
-
States a default rather than a fixed vocabulary. A session may write the header
|
|
98
|
+
States a default rather than a fixed vocabulary. A session may write the header
|
|
99
|
+
in its own words, since it exists to be read once and copied rather than parsed
|
|
100
|
+
by anything downstream. Name the same three parts however phrased.
|
|
34
101
|
|
|
35
102
|
## Output
|
|
36
103
|
|
|
37
|
-
|
|
104
|
+
The sent message, or the composed block and nothing beyond it. Report Step 1's
|
|
105
|
+
refusal in place of a sender the roster cannot answer for, and report the halt
|
|
106
|
+
or the inference in place of an addressee Step 2 cannot resolve.
|
package/docs/agents/tasks.md
CHANGED
|
@@ -54,6 +54,8 @@ An outcome whose body is struck reads as cut rather than open or closed, whateve
|
|
|
54
54
|
|
|
55
55
|
The task carries its plan with it. When the closing task is the last live one whose `Plan:` line resolves onto that file, the plan moves to `.canon/plans/archive/` under its own name and the archived task's line is rewritten as `Plan: [feature-<slug>](../../plans/archive/feature-<slug>.md)`, a folder deeper than the live task wrote it. The `plan` field on the success record carries that `from` and `to`, and is `null` when nothing moved.
|
|
56
56
|
|
|
57
|
+
Every other relative link in the task is re-resolved for the deeper folder too, whether its target moved or not: `Groundwork:`, `Intake:`, a `Plan:` or `Ready:` line whose target stayed put, and a link in the body. Fenced blocks, inline code spans, absolute URLs, `#` anchors, and rooted paths are left alone. `canon tasks decline` applies the same rebase, since `declined/` sits at the same depth. The rebase runs before the plan and ready retargets, so a moved plan keeps its own line.
|
|
58
|
+
|
|
57
59
|
The ready folder the task's `Ready:` line names moves with the plan to `.canon/ready/archive/`, falling back to a `.canon/ready/<nn>-<slug>/` path in the plan when the line is absent. The `Ready:` line and the plan's literal path to the folder are retargeted, and the `ready` field on the success record carries `from` and `to`, or `null` when nothing moved. A folder that resolves to nothing, sits already archived, or meets a taken destination moves nothing and refuses nothing.
|
|
58
60
|
|
|
59
61
|
A plan several tasks share stays where it is, and its ready folder with it, and the task archives anyway. Moving it on the first task to close strands every sibling's pointer at a path that has gone, and `.canon/plans/` is gitignored so no history recovers the target. A `Plan:` line resolving to no file leaves the plan alone too, since a pointer somebody typed wrong is not a plan to move and holding the whole archive over it would park the board behind a repair the merge cannot make.
|
|
@@ -272,15 +272,15 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
272
272
|
|
|
273
273
|
### Run several tracks at once
|
|
274
274
|
|
|
275
|
-
| Skill | When to use
|
|
276
|
-
| ------------------------- |
|
|
277
|
-
| `canon:role-orchestrator` | To assert the control session that owns the queue and reviews each worker's PR
|
|
278
|
-
| `canon:role-planner` | To assert the
|
|
279
|
-
| `canon:role-worker` | To assert the worker role for a cold session building one branch under one plan
|
|
280
|
-
| `canon:session-relay` | When
|
|
281
|
-
| `canon:session-resume` | At the start of a session, to pick up what a previous one left
|
|
282
|
-
| `canon:session-compact` | Before a compaction, to write a plain session's handoff note outside the board
|
|
283
|
-
| `canon:session-map` | At the close of an orchestrating session, to write its board-side handoff
|
|
275
|
+
| Skill | When to use |
|
|
276
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `canon:role-orchestrator` | To assert the control session that owns the queue and reviews each worker's PR |
|
|
278
|
+
| `canon:role-planner` | To assert the thinking role for a cold session writing a plan, a groundwork track, or an intake pass |
|
|
279
|
+
| `canon:role-worker` | To assert the worker role for a cold session building one branch under one plan |
|
|
280
|
+
| `canon:session-relay` | When this session owes another session a message, with or without a role |
|
|
281
|
+
| `canon:session-resume` | At the start of a session, to pick up what a previous one left |
|
|
282
|
+
| `canon:session-compact` | Before a compaction, to write a plain session's handoff note outside the board |
|
|
283
|
+
| `canon:session-map` | At the close of an orchestrating session, to write its board-side handoff |
|
|
284
284
|
|
|
285
285
|
### Keep the project current with the toolkit
|
|
286
286
|
|
|
@@ -42,7 +42,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
42
42
|
- `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
43
43
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
44
44
|
- `canon:ux-walkthrough` for a multi-finding inspection pass over a running app with the operator, drafting arms from the app's own markup and recording each pick for a builder
|
|
45
|
-
- `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
45
|
+
- `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop. On an Astro project holding the card route scaffold, the card lands as a route the project keeps and is captured through a running server, so it reads the project's real stylesheet rather than typed copies of every color. Everywhere else it ships as a PNG and the run says so.
|
|
46
46
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
47
47
|
|
|
48
48
|
### When to pick
|
package/package.json
CHANGED
|
@@ -155,7 +155,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
prompt:
|
|
158
|
-
'
|
|
158
|
+
'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.',
|
|
159
159
|
expect: 'session-relay',
|
|
160
160
|
},
|
|
161
161
|
|
package/src/gate/measures.ts
CHANGED
|
@@ -818,9 +818,17 @@ export const readmeCitations: Measure = async (ctx) => {
|
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Globs `pr-visual-checks.yml` carries with no `deploy-site.yml` counterpart,
|
|
823
|
+
* because the path changes no byte of the built site. The capture harness is
|
|
824
|
+
* the one case: a change there should report the capture job and never deploy.
|
|
825
|
+
*/
|
|
826
|
+
const VISUAL_ONLY_GLOBS: readonly string[] = ['tooling/web/configs/e2e/**']
|
|
827
|
+
|
|
821
828
|
/**
|
|
822
829
|
* `deploy-site.yml` and `pr-visual-checks.yml` each carry their own literal
|
|
823
|
-
* copy of the eight path globs a landing-page change should trigger on,
|
|
830
|
+
* copy of the eight path globs a landing-page change should trigger on, plus
|
|
831
|
+
* the `VISUAL_ONLY_GLOBS` the visual workflow adds for itself, per
|
|
824
832
|
* Question 2 in `.canon/plans/archive/feature-render-visibility.md`. A YAML
|
|
825
833
|
* anchor cannot cross files here, so the two copies are read live and compared
|
|
826
834
|
* rather than one asserting a literal the other could drift behind unnoticed,
|
|
@@ -858,14 +866,25 @@ export const visualPathGlobs: Measure = async (ctx) => {
|
|
|
858
866
|
}
|
|
859
867
|
}
|
|
860
868
|
|
|
861
|
-
const onlyDeploy = deployPaths.filter(
|
|
862
|
-
|
|
869
|
+
const onlyDeploy = deployPaths.filter(
|
|
870
|
+
(path) => !visualPaths.includes(path) || VISUAL_ONLY_GLOBS.includes(path),
|
|
871
|
+
)
|
|
872
|
+
const onlyVisual = visualPaths.filter(
|
|
873
|
+
(path) => !deployPaths.includes(path) && !VISUAL_ONLY_GLOBS.includes(path),
|
|
874
|
+
)
|
|
875
|
+
const declaredVisualOnly = visualPaths.filter((path) =>
|
|
876
|
+
VISUAL_ONLY_GLOBS.includes(path),
|
|
877
|
+
)
|
|
863
878
|
|
|
864
879
|
if (onlyDeploy.length === 0 && onlyVisual.length === 0) {
|
|
880
|
+
const visualOnlyNote =
|
|
881
|
+
declaredVisualOnly.length > 0
|
|
882
|
+
? `, plus visual-only ${declaredVisualOnly.join(', ')}`
|
|
883
|
+
: ''
|
|
865
884
|
return {
|
|
866
885
|
emissions: [
|
|
867
886
|
info(
|
|
868
|
-
`${deployPaths.length} path glob(s) agree between ${deployPath} and ${visualPath}`,
|
|
887
|
+
`${deployPaths.length} path glob(s) agree between ${deployPath} and ${visualPath}${visualOnlyNote}`,
|
|
869
888
|
),
|
|
870
889
|
],
|
|
871
890
|
}
|
|
@@ -874,7 +893,11 @@ export const visualPathGlobs: Measure = async (ctx) => {
|
|
|
874
893
|
return {
|
|
875
894
|
emissions: [
|
|
876
895
|
...onlyDeploy.map((path) =>
|
|
877
|
-
warn(
|
|
896
|
+
warn(
|
|
897
|
+
VISUAL_ONLY_GLOBS.includes(path)
|
|
898
|
+
? `${deployPath} carries ${path}, which is visual-only and must not trigger a deploy`
|
|
899
|
+
: `${deployPath} carries ${path}, absent from ${visualPath}`,
|
|
900
|
+
),
|
|
878
901
|
),
|
|
879
902
|
...onlyVisual.map((path) =>
|
|
880
903
|
warn(`${visualPath} carries ${path}, absent from ${deployPath}`),
|
package/src/tasks/archive.ts
CHANGED
|
@@ -792,15 +792,16 @@ export async function archiveTask(
|
|
|
792
792
|
|
|
793
793
|
await mkdir(destination, { recursive: true })
|
|
794
794
|
await rename(from, to)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
795
|
+
const rebased = rebaseRelativeLinks(text, dir, destination)
|
|
796
|
+
const retargeted = plan
|
|
797
|
+
? retargetPlanLine(rebased, linkTo(destination, plan.to))
|
|
798
|
+
: rebased
|
|
799
|
+
await writeFile(
|
|
800
|
+
to,
|
|
801
|
+
ready
|
|
802
|
+
? retargetReadyLine(retargeted, `${linkTo(destination, ready.to)}/`)
|
|
803
|
+
: retargeted,
|
|
804
|
+
)
|
|
804
805
|
|
|
805
806
|
const priorityRowRemoved = await clearPriorityRow(dir, stem)
|
|
806
807
|
const regen = await regenOne(dir, { dryRun: false })
|
|
@@ -918,6 +919,69 @@ export function linkTo(taskDir: string, plan: string): string {
|
|
|
918
919
|
return relative(taskDir, plan).split(sep).join('/')
|
|
919
920
|
}
|
|
920
921
|
|
|
922
|
+
const MARKDOWN_LINK_PATTERN = /(\]\()([^)\s]+)/g
|
|
923
|
+
/** The capture group makes `split` keep each code span, at the odd positions. */
|
|
924
|
+
const INLINE_CODE_PATTERN = /(`+[^`]*`+)/
|
|
925
|
+
const BARE_ORIGIN_PATTERN =
|
|
926
|
+
/^((?:Plan|Ready|Groundwork|Intake):[ \t]*)([^\s[\]()]+)[ \t]*$/
|
|
927
|
+
|
|
928
|
+
function isRelativeTarget(target: string): boolean {
|
|
929
|
+
return !/^(?:[a-z][a-z0-9+.-]*:|\/|#)/i.test(target)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function rebaseTarget(target: string, fromDir: string, toDir: string): string {
|
|
933
|
+
if (!isRelativeTarget(target)) return target
|
|
934
|
+
|
|
935
|
+
const suffixAt = target.search(/[#?]/)
|
|
936
|
+
const path = suffixAt === -1 ? target : target.slice(0, suffixAt)
|
|
937
|
+
const suffix = suffixAt === -1 ? '' : target.slice(suffixAt)
|
|
938
|
+
if (path === '') return target
|
|
939
|
+
|
|
940
|
+
const rebased = relative(toDir, resolve(fromDir, path)).split(sep).join('/')
|
|
941
|
+
const slash = path.endsWith('/') ? '/' : ''
|
|
942
|
+
|
|
943
|
+
return `${rebased}${slash}${suffix}`
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Re-resolves every relative link in a task written for `fromDir` so it reads
|
|
948
|
+
* the same target from `toDir`. Every link rather than the four origin lines,
|
|
949
|
+
* since the body breaks one folder short the same way. Fenced samples stay
|
|
950
|
+
* verbatim, and a URL, an anchor, or a rooted path names nothing depth changes.
|
|
951
|
+
*/
|
|
952
|
+
export function rebaseRelativeLinks(
|
|
953
|
+
text: string,
|
|
954
|
+
fromDir: string,
|
|
955
|
+
toDir: string,
|
|
956
|
+
): string {
|
|
957
|
+
const lines = text.split('\n')
|
|
958
|
+
const fenced = fenceMask(lines)
|
|
959
|
+
|
|
960
|
+
return lines
|
|
961
|
+
.map((line, index) => {
|
|
962
|
+
if (fenced[index]) return line
|
|
963
|
+
|
|
964
|
+
const bare = BARE_ORIGIN_PATTERN.exec(line)
|
|
965
|
+
if (bare?.[2].includes('/')) {
|
|
966
|
+
return `${bare[1]}${rebaseTarget(bare[2], fromDir, toDir)}`
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return line
|
|
970
|
+
.split(INLINE_CODE_PATTERN)
|
|
971
|
+
.map((part, position) =>
|
|
972
|
+
position % 2 === 1
|
|
973
|
+
? part
|
|
974
|
+
: part.replace(
|
|
975
|
+
MARKDOWN_LINK_PATTERN,
|
|
976
|
+
(_whole: string, open: string, target: string) =>
|
|
977
|
+
`${open}${rebaseTarget(target, fromDir, toDir)}`,
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
.join('')
|
|
981
|
+
})
|
|
982
|
+
.join('\n')
|
|
983
|
+
}
|
|
984
|
+
|
|
921
985
|
async function clearPriorityRow(dir: string, stem: string): Promise<boolean> {
|
|
922
986
|
const path = join(dir, 'priority.md')
|
|
923
987
|
if (!existsSync(path)) return false
|
|
@@ -984,7 +1048,10 @@ export async function declineTask(
|
|
|
984
1048
|
await rename(from, to)
|
|
985
1049
|
|
|
986
1050
|
const date = new Date().toISOString().slice(0, 10)
|
|
987
|
-
const declined = insertDeclinedLine(
|
|
1051
|
+
const declined = insertDeclinedLine(
|
|
1052
|
+
rebaseRelativeLinks(text, dir, destination),
|
|
1053
|
+
declineLine(reason, by, date),
|
|
1054
|
+
)
|
|
988
1055
|
const final = plan
|
|
989
1056
|
? retargetPlanLine(declined, linkTo(destination, plan.to))
|
|
990
1057
|
: declined
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The social card as a route the project keeps, rather than a scratch page a
|
|
4
|
+
* render deletes. `canon capture` drives it through the server this config
|
|
5
|
+
* serves, so the composition reads the project's own stylesheet, tokens and
|
|
6
|
+
* fonts instead of carrying typed copies of each. Renaming the project is then
|
|
7
|
+
* an edit to one string here and a re-capture, not another pass through the
|
|
8
|
+
* pick loop with an operator in it.
|
|
9
|
+
*
|
|
10
|
+
* `canon:draft-identity` writes the picked composition into the `.card`
|
|
11
|
+
* element below and leaves everything around it alone. What ships is a working
|
|
12
|
+
* placeholder, so the route serves before any pick has been taken.
|
|
13
|
+
*
|
|
14
|
+
* The card is declared at half of 1200x630. `canon capture` opens every page at
|
|
15
|
+
* a fixed 2x device scale factor and screenshots the element at that scale, so
|
|
16
|
+
* a 600x315 element is what lands the captured PNG on the literal target size.
|
|
17
|
+
* Declaring it at 1200x630 captures at 2400x1260.
|
|
18
|
+
*
|
|
19
|
+
* `card.config.mjs` is the only config whose page router reaches this file, and
|
|
20
|
+
* `scripts/check-card-exclusion.sh` fails a build that carries the marker
|
|
21
|
+
* below, which is what keeps the route out of the published site.
|
|
22
|
+
*/
|
|
23
|
+
import '@/styles/global.css'
|
|
24
|
+
|
|
25
|
+
// Retarget the import above when this project keeps its global stylesheet
|
|
26
|
+
// somewhere else. An unresolved path refuses the card server by name, which is
|
|
27
|
+
// the loud direction: a card captured against no stylesheet renders as an
|
|
28
|
+
// unstyled box and reports success.
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="utf-8" />
|
|
34
|
+
<title>Social card</title>
|
|
35
|
+
<!-- Read by scripts/check-card-exclusion.sh. A build carrying this tag
|
|
36
|
+
published the card route. Do not reword it. -->
|
|
37
|
+
<meta name="og-card-marker" content="og-card-marker" />
|
|
38
|
+
<style>
|
|
39
|
+
body {
|
|
40
|
+
margin: 0;
|
|
41
|
+
}
|
|
42
|
+
.card {
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
width: 600px;
|
|
45
|
+
height: 315px;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
gap: 0.5rem;
|
|
50
|
+
padding: 3rem;
|
|
51
|
+
/* These two names are what `canon design css` emits, so a project
|
|
52
|
+
carrying the design files resolves both and neither fallback fires.
|
|
53
|
+
A name the design system does not emit would take its fallback,
|
|
54
|
+
capture cleanly and report success, which is the silent failure this
|
|
55
|
+
route exists to close. Check the names against the stylesheet when
|
|
56
|
+
retargeting the import above.
|
|
57
|
+
|
|
58
|
+
Written as rgb() rather than hex so the shipped-reference gate does
|
|
59
|
+
not read a six-digit hex fallback as a pull request number. */
|
|
60
|
+
background: var(--color-background, rgb(255 255 255));
|
|
61
|
+
color: var(--color-text, rgb(17 17 17));
|
|
62
|
+
/* No font-family here on purpose. The design system emits no font
|
|
63
|
+
token, so the card inherits the project's own body font rather than
|
|
64
|
+
reading a custom property that does not exist. `canon capture` reads
|
|
65
|
+
the computed family off this element, so inheritance still gives it
|
|
66
|
+
a family to check and it refuses when that family cannot resolve. */
|
|
67
|
+
}
|
|
68
|
+
.mark {
|
|
69
|
+
width: 64px;
|
|
70
|
+
height: 64px;
|
|
71
|
+
}
|
|
72
|
+
.title {
|
|
73
|
+
margin: 0;
|
|
74
|
+
font-size: 2.25rem;
|
|
75
|
+
line-height: 1.1;
|
|
76
|
+
}
|
|
77
|
+
.tagline {
|
|
78
|
+
margin: 0;
|
|
79
|
+
font-size: 1.125rem;
|
|
80
|
+
opacity: 0.75;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<div class="card">
|
|
86
|
+
<svg
|
|
87
|
+
class="mark"
|
|
88
|
+
viewBox="0 0 64 64"
|
|
89
|
+
role="img"
|
|
90
|
+
aria-label="Project mark"
|
|
91
|
+
>
|
|
92
|
+
<rect width="64" height="64" rx="12" fill="currentColor"></rect>
|
|
93
|
+
</svg>
|
|
94
|
+
<h1 class="title">REPLACE_WITH_PROJECT_NAME</h1>
|
|
95
|
+
<p class="tagline">REPLACE_WITH_PROJECT_TAGLINE</p>
|
|
96
|
+
</div>
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import react from '@astrojs/react'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
import { defineConfig } from 'astro/config'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
const portOffset = Number(process.env.WORKTREE_PORT_OFFSET) || 0
|
|
7
|
+
|
|
8
|
+
// Never read by `bun run build`. srcDir keeps this config's page router off
|
|
9
|
+
// src/pages entirely, which is what excludes the social card from the published
|
|
10
|
+
// build structurally rather than by a filename convention. A pageExtensions
|
|
11
|
+
// regex over a filename was the alternative, and it leaks the moment a route is
|
|
12
|
+
// renamed.
|
|
13
|
+
//
|
|
14
|
+
// The base port sits outside the band `astro.config.mjs` serves from, so a card
|
|
15
|
+
// server and a dev server of one worktree never contend, and two worktrees
|
|
16
|
+
// separate here the way they separate there.
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
srcDir: './card-src',
|
|
19
|
+
outDir: './card-dist',
|
|
20
|
+
publicDir: './public',
|
|
21
|
+
integrations: [react()],
|
|
22
|
+
server: {
|
|
23
|
+
port: 4421 + portOffset,
|
|
24
|
+
},
|
|
25
|
+
vite: {
|
|
26
|
+
plugins: [tailwindcss()],
|
|
27
|
+
resolve: {
|
|
28
|
+
alias: {
|
|
29
|
+
'@': path.resolve('./src'),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
server: {
|
|
33
|
+
strictPort: true,
|
|
34
|
+
},
|
|
35
|
+
preview: {
|
|
36
|
+
strictPort: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
set -o pipefail
|
|
4
|
+
|
|
5
|
+
# `card.config.mjs` keeps the social card route out of the published build by
|
|
6
|
+
# never pointing its srcDir at it. A later config change that widens srcDir, or
|
|
7
|
+
# a card file moving under src/pages/, would publish a half-size scaffolding
|
|
8
|
+
# page to a live domain with every other check still green.
|
|
9
|
+
#
|
|
10
|
+
# The card route has three consumers: the card server reads it, the capture
|
|
11
|
+
# reads it, and the published build must not carry it. This is the only one of
|
|
12
|
+
# the three a check can enforce, so the route's exclusion is a check rather than
|
|
13
|
+
# a convention.
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
17
|
+
|
|
18
|
+
DIST="$PROJECT_ROOT/dist"
|
|
19
|
+
CARD_CONFIG="$PROJECT_ROOT/card.config.mjs"
|
|
20
|
+
MARKER='name="og-card-marker"'
|
|
21
|
+
|
|
22
|
+
# No card route installed, so there is nothing to exclude. Reported rather than
|
|
23
|
+
# passed silently, since a check that says nothing is indistinguishable from one
|
|
24
|
+
# that looked and found nothing.
|
|
25
|
+
if [ ! -f "$CARD_CONFIG" ]; then
|
|
26
|
+
echo "check-card-exclusion: no card.config.mjs, so no card route to exclude"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Nothing to grep without a build. Callers run this after `bun run build`, so an
|
|
31
|
+
# absent dist here means the build itself failed rather than something this
|
|
32
|
+
# check can verify.
|
|
33
|
+
if [ ! -d "$DIST" ]; then
|
|
34
|
+
echo "No $DIST. Run bun run build first." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# The marker is a fixed <meta> tag the card route carries for exactly this, so
|
|
39
|
+
# asserting it survives in the route's own source is what turns a dropped marker
|
|
40
|
+
# into a loud failure here rather than a blind spot in the test below. A prose
|
|
41
|
+
# notice is not load-bearing: rewording it silently retires the leak check it
|
|
42
|
+
# backed with every workflow still green.
|
|
43
|
+
if ! grep -rl "$MARKER" "$PROJECT_ROOT/card-src" >/dev/null 2>&1; then
|
|
44
|
+
echo "No $MARKER under $PROJECT_ROOT/card-src. The card route dropped its own marker, which is what the leak test below reads for." >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if grep -rl "$MARKER" "$DIST" >/dev/null 2>&1; then
|
|
49
|
+
echo "The card route's marker ($MARKER) reached $DIST. See card.config.mjs's srcDir." >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if find "$DIST" -iname '*og-card*' | grep -q .; then
|
|
54
|
+
echo "A file named for the card route reached $DIST. See card.config.mjs's srcDir." >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "check-card-exclusion: card marker present in card-src, absent from $DIST"
|
|
@@ -21,6 +21,8 @@ packages = [
|
|
|
21
21
|
"astro" = "astro"
|
|
22
22
|
"typecheck" = "astro check"
|
|
23
23
|
"setup" = "./scripts/setup.sh"
|
|
24
|
+
"card:dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev --config card.config.mjs"
|
|
25
|
+
"card:check" = "bash scripts/check-card-exclusion.sh"
|
|
24
26
|
|
|
25
27
|
[scripts.override]
|
|
26
28
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
@@ -30,6 +32,7 @@ packages = [
|
|
|
30
32
|
|
|
31
33
|
[gitignore]
|
|
32
34
|
"# Astro" = [".astro/"]
|
|
35
|
+
"# Social card" = ["card-dist/"]
|
|
33
36
|
|
|
34
37
|
[verify]
|
|
35
38
|
prepare = "bunx astro add react --yes"
|
|
@@ -21,6 +21,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
21
21
|
- `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
|
|
22
22
|
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
|
|
23
23
|
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
|
|
24
|
+
- `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`: the social card route, its structural build exclusion, and the check enforcing it. See `## Social card route` below.
|
|
24
25
|
- `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `canon/context/tooling.md` for the measurement.
|
|
25
26
|
|
|
26
27
|
## Typecheck
|
|
@@ -59,11 +60,37 @@ Append to the `## Scripts` table:
|
|
|
59
60
|
|
|
60
61
|
In `canon/context/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``.
|
|
61
62
|
|
|
63
|
+
## Social card route
|
|
64
|
+
|
|
65
|
+
The social card is a route the project keeps rather than a page a render writes and deletes, so re-rendering it after a rename is an edit to one string and a re-capture instead of another pass through `draft-identity`'s pick loop with an operator in it. `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
|
|
66
|
+
|
|
67
|
+
### What ships
|
|
68
|
+
|
|
69
|
+
- `card.config.mjs`: a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That is what excludes the card from the published build structurally rather than by a filename convention, the same mechanism `scenarios.astro` reaches for with `import.meta.env.DEV`. Its base port is `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a card server and a dev server of one worktree never contend.
|
|
70
|
+
- `card-src/pages/og-card.astro`: the route, shipped as a working placeholder so it serves before any pick has been taken. It imports the project's own global stylesheet, which is what lets the composition read real tokens and real fonts instead of typed copies. It reads `--color-background` and `--color-text`, the names `canon design css` emits, and sets no `font-family` at all so the card inherits the project's body font rather than reading a font token the design system does not emit.
|
|
71
|
+
- `scripts/check-card-exclusion.sh`: fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and fails when the route dropped that tag, since the marker is what the leak test reads for.
|
|
72
|
+
- `bun run card:dev` serves the route. `bun run card:check` runs the exclusion check, which the web layer's `scripts/verify.sh` also runs after its build stage whenever the script is installed.
|
|
73
|
+
|
|
74
|
+
### Why each piece is shaped that way
|
|
75
|
+
|
|
76
|
+
The stylesheet import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line, and an unresolved path then refuses the card server by name, which is the loud direction rather than the convenient one. The quiet failure it replaces is a card captured against no stylesheet at all: it renders as an unstyled box and reports success.
|
|
77
|
+
|
|
78
|
+
A custom property the stylesheet does not define is the same silent failure in a second costume. It takes its fallback, captures cleanly and reports success, so the card ships on typed values while reading as though it read real ones. That is why the route names only properties `canon design css` emits, and why a project retargeting the stylesheet import checks the names against what its own stylesheet defines.
|
|
79
|
+
|
|
80
|
+
The exclusion is a check rather than a convention because the route has three consumers and only one of them can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
|
|
81
|
+
|
|
82
|
+
`.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element is what lands the captured PNG on the literal size. Declaring it at 1200x630 captures at 2400x1260.
|
|
83
|
+
|
|
84
|
+
### Why v1 is Astro-only
|
|
85
|
+
|
|
86
|
+
Astro is the one web stack whose config gives a route a structural build exclusion. Next's App Router has no build-time route exclusion at all, and it needs none on the path it will take, since `ImageResponse` is that framework's own production mechanism and a card route there is meant to be served. `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
|
|
87
|
+
|
|
62
88
|
## Gitignore (extend)
|
|
63
89
|
|
|
64
90
|
`[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
|
|
65
91
|
|
|
66
92
|
- `"# Astro" = [".astro/"]`
|
|
93
|
+
- `"# Social card" = ["card-dist/"]`
|
|
67
94
|
|
|
68
95
|
## Scenario switcher
|
|
69
96
|
|
|
@@ -71,6 +71,15 @@ main() {
|
|
|
71
71
|
run_check "bun run build" "Build failed"
|
|
72
72
|
log_info "Build passed"
|
|
73
73
|
|
|
74
|
+
# Runs whenever the stack shipped a card exclusion check, which keeps the
|
|
75
|
+
# stage stack-agnostic here and the check itself in the stack that owns the
|
|
76
|
+
# card route's shape. It reads the build above, so it sits after it.
|
|
77
|
+
if [ -f scripts/check-card-exclusion.sh ]; then
|
|
78
|
+
log_step "Card exclusion"
|
|
79
|
+
run_check "bash scripts/check-card-exclusion.sh" "Card exclusion check failed"
|
|
80
|
+
log_info "Card exclusion passed"
|
|
81
|
+
fi
|
|
82
|
+
|
|
74
83
|
if [ "$NESTED" = false ]; then
|
|
75
84
|
echo -e "${GREY}└${NC}\n"
|
|
76
85
|
echo -e "${GREEN}✓ Verification passed${NC}"
|
package/tooling/web/reference.md
CHANGED
|
@@ -19,7 +19,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
19
19
|
- `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/ci/700-ci-workflow.md` states the test.
|
|
20
20
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
21
21
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
22
|
-
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
|
|
22
|
+
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order. Its last stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which puts the social card route's build exclusion on the default path rather than behind a flag nobody passes. The stage tests for the script rather than for a framework, so this layer owns when the check runs and the stack shipping a card route owns what it reads. The astro reference carries the route itself.
|
|
23
23
|
- `scripts/worktree-port.sh`: prints a base port plus this working directory's offset. Called with no argument it prints the offset alone. It refuses a folder left under the worktrees directory after its worktree was removed, rather than printing a port for it.
|
|
24
24
|
|
|
25
25
|
## What stays in per-stack adapters
|
|
@@ -128,4 +128,6 @@ The sweep under `screenshots/` is ignored again, and only a flagged case's `evid
|
|
|
128
128
|
|
|
129
129
|
## Verify script
|
|
130
130
|
|
|
131
|
-
The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build. Stack adapters may override if their typecheck or build differs.
|
|
131
|
+
The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build, then the card exclusion check where one is installed. Stack adapters may override if their typecheck or build differs.
|
|
132
|
+
|
|
133
|
+
The card stage is conditional on `scripts/check-card-exclusion.sh` existing rather than on the stack, so a project with no card route runs one less stage and needs no override to skip it. A stack that ships a card route ships that script, and the check reads the build the stage before it produced.
|