@erclx/canon 4.70.0 → 4.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +4 -0
- package/claude/skills/deploy-cloudflare/REQUIREMENT.md +37 -0
- package/claude/skills/deploy-cloudflare/SKILL.md +61 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +3 -1
- package/claude/skills/draft-and-pick/SKILL.md +10 -6
- package/claude/skills/{identity → draft-identity}/REQUIREMENT.md +2 -2
- package/claude/skills/{identity → draft-identity}/SKILL.md +2 -2
- package/claude/skills/git-ship/SKILL.md +17 -6
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -0
- package/claude/skills/role-planner/SKILL.md +5 -0
- package/claude/skills/role-worker/SKILL.md +6 -0
- package/claude/skills/session-relay/REQUIREMENT.md +41 -0
- package/claude/skills/session-relay/SKILL.md +36 -0
- package/claude/skills/ux-audit/SKILL.md +3 -0
- package/docs/agents/key-changes.md +5 -1
- package/docs/agents/tasks.md +33 -0
- package/docs/workflow/ai-workflow.md +2 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/ui/440-surface-capture.md +1 -0
- package/package.json +1 -1
- package/src/claude/cases/misc.ts +5 -1
- package/src/claude/cases/workflow.ts +5 -0
- package/src/commands/pr.ts +128 -6
- package/src/commands/tasks.ts +154 -0
- package/src/commands/teach.ts +2 -0
- package/src/gate/measures.ts +66 -0
- package/src/gate/stages.ts +11 -0
- package/src/git-files.ts +69 -0
- package/src/migrate/skill-names.ts +10 -0
- package/src/pr/bijection.ts +59 -1
- package/src/tasks/reach.ts +383 -0
- package/src/teach/workspace.ts +60 -23
- package/standards/plan.md +2 -0
- package/standards/tasks.md +2 -0
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +103 -0
- package/tooling/cloudflare/manifest.toml +5 -0
- package/tooling/cloudflare/reference.md +24 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ canon is a CLI and Claude Code plugin that stops your AI conventions drifting ap
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
|
|
11
|
+
The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today. See it live at [canon.erclx.dev](https://canon.erclx.dev).
|
|
12
12
|
|
|
13
13
|
## It runs on itself
|
|
14
14
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-cloudflare
|
|
3
|
+
description: What the Cloudflare Pages setup gap is, which of the four manual steps this skill closes, and why the credential and the custom domain stay the operator's
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy Cloudflare requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a Cloudflare Pages setup is a sequence performed from memory. Three of its four repeated acts touch a service rather than the tree, so no workflow file can absorb them, and one of the acts has already cost a broken deploy: the account ID displayed in the Cloudflare dashboard is truncated, and a value copied from there is wrong in a way that only shows up at deploy time.
|
|
11
|
+
|
|
12
|
+
## Must
|
|
13
|
+
|
|
14
|
+
- Create the Pages project through `wrangler pages project create`
|
|
15
|
+
- Fetch the account ID from `wrangler`'s own authenticated session rather than have it typed or pasted
|
|
16
|
+
- Stop for the operator to issue the API token and run `gh secret set CLOUDFLARE_API_TOKEN` themselves
|
|
17
|
+
- Verify both secrets are present with `gh secret list` before continuing
|
|
18
|
+
- Stop for the operator to attach the custom domain in the dashboard
|
|
19
|
+
- Call `wrangler` and `gh` directly for every step those tools already cover
|
|
20
|
+
|
|
21
|
+
## Must not
|
|
22
|
+
|
|
23
|
+
- Accept a Cloudflare API token as input in any form. The toolkit verifies a secret exists, never what it contains.
|
|
24
|
+
- Reimplement `wrangler pages project create` or `gh secret set` as a hand-built API call
|
|
25
|
+
- Open a pull request or merge
|
|
26
|
+
|
|
27
|
+
## Guards
|
|
28
|
+
|
|
29
|
+
- `wrangler` not authenticated: stop and name the login command
|
|
30
|
+
- `gh` not authenticated: stop and name the login command
|
|
31
|
+
- A secret missing after the token stop: stop and name which one
|
|
32
|
+
|
|
33
|
+
## Out of scope
|
|
34
|
+
|
|
35
|
+
- Attaching a custom domain through the Cloudflare REST API instead of the dashboard. Measured absent from `wrangler pages` at plan time and the REST API was not read, so the dashboard stop stays for this pass.
|
|
36
|
+
- Running the deploy itself once secrets and the domain are set. That is the seeded `tooling/cloudflare/configs/.github/workflows/deploy.yml` workflow, triggered by a push to main.
|
|
37
|
+
- Reconfiguring an existing Pages project's build settings.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-cloudflare
|
|
3
|
+
description: Sets up a Cloudflare Pages deploy for the current project by creating the Pages project, fetching the account ID, and stopping twice for the operator to issue the API token and attach the custom domain. Calls `wrangler` and `gh` rather than reimplementing them. Use when asked to "set up Cloudflare deploy", "deploy this to Cloudflare Pages", "set up the Pages project", or "connect this repo to Cloudflare". Do NOT use to run an already-configured deploy, which the seeded workflow does on push, or to reconfigure an existing Pages project's build settings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy Cloudflare
|
|
7
|
+
|
|
8
|
+
Runs the one-time setup a Cloudflare Pages deploy needs before the seeded `deploy.yml` workflow can run, stopping twice for the two acts only the operator can take.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If `wrangler whoami` fails, stop: `❌ wrangler is not authenticated. Run wrangler login, then re-invoke.`
|
|
13
|
+
- If `gh auth status` fails, stop: `❌ gh is not authenticated. Run gh auth login, then re-invoke.`
|
|
14
|
+
- Never accept a Cloudflare API token as input, in a prompt, an argument, or a file. Verify a secret's presence with `gh secret list` rather than asking for or reading its content.
|
|
15
|
+
- Never reimplement `wrangler pages project create` or `gh secret set` as a hand-built HTTP call. Call the tools directly.
|
|
16
|
+
- If the project name is not supplied, derive it from the repository's own name (`basename` of `git remote get-url origin`, stripped of a trailing `.git`) and confirm it in the preview rather than asking first.
|
|
17
|
+
|
|
18
|
+
## Step 1: create the Pages project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
wrangler pages project create <project-name>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the command's own failure output and stop rather than retrying on a name collision. A project already existing under that name is the operator's to resolve.
|
|
25
|
+
|
|
26
|
+
## Step 2: fetch and set the account ID
|
|
27
|
+
|
|
28
|
+
Fetch the account ID from `wrangler`'s own authenticated session rather than asking the operator to copy it from the dashboard. The dashboard displays the account ID truncated, and a value copied from there fails the deploy silently:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
wrangler whoami
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Parse the account ID from that output and pipe it into the secret rather than printing it to the transcript first:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
echo "<account-id>" | gh secret set CLOUDFLARE_ACCOUNT_ID
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Step 3: stop for the API token
|
|
41
|
+
|
|
42
|
+
Stop: `⏸ Create a Cloudflare API token with Pages edit permission at the Cloudflare dashboard, then run: gh secret set CLOUDFLARE_API_TOKEN. Re-invoke this skill once that's done.`
|
|
43
|
+
|
|
44
|
+
This is the one credential the skill never touches. Resume only once the operator confirms the token is set.
|
|
45
|
+
|
|
46
|
+
## Step 4: verify both secrets
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gh secret list
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Both `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` present: continue.
|
|
53
|
+
- Either missing: stop and name which one, with the command that sets it.
|
|
54
|
+
|
|
55
|
+
## Step 5: stop for the custom domain
|
|
56
|
+
|
|
57
|
+
Stop: `⏸ Attach the custom domain to the <project-name> Pages project in the Cloudflare dashboard, then confirm here. Whether this can run through the API instead of the dashboard is out of scope for this skill, per its REQUIREMENT.md.`
|
|
58
|
+
|
|
59
|
+
## Step 6: hand off
|
|
60
|
+
|
|
61
|
+
Report that setup is complete and name the seeded workflow (`tooling/cloudflare/configs/.github/workflows/deploy.yml`, synced via `canon tooling sync cloudflare . --write`) as what deploys on the next push. Do not invoke `git-pr` or `git-ship` from here. The operator or the controlling session decides when to open that pull request.
|
|
@@ -28,7 +28,9 @@ Without this skill, a session facing a decision nobody can settle from a diff:
|
|
|
28
28
|
- Render before reporting on any arm, and report a missing browser binary as a refusal naming the repair rather than describing an arm nobody has seen.
|
|
29
29
|
- Take the pick through the structured question surface, with the recommendation ranked first and marked, and every option carrying its cost.
|
|
30
30
|
- Keep every write inside the run's scratch folder until the pick is final.
|
|
31
|
-
- Apply the winning arm
|
|
31
|
+
- Apply the winning arm in one change.
|
|
32
|
+
- Capture every arm from the final round as an image before deleting the run's scratch folder, rather than discarding the losing arms with it.
|
|
33
|
+
- Resolve the archival capture's destination against the main worktree root, never against a linked worktree the run happens to be building in.
|
|
32
34
|
- Serve the candidates live where the decision is one the operator has to drive, since a still cannot answer how a gesture feels or how a pace reads.
|
|
33
35
|
|
|
34
36
|
## Must not
|
|
@@ -23,9 +23,10 @@ Some decisions are settled by looking rather than by reasoning, and no draft is
|
|
|
23
23
|
|
|
24
24
|
## Step 2: author the candidate set as one page
|
|
25
25
|
|
|
26
|
-
Write every arm side by side on one self-contained HTML page at `<dest>/candidates.html`.
|
|
26
|
+
Write every arm side by side on one self-contained HTML page at `<dest>/candidates.html`, and write each arm again as its own self-contained whole-page file at `<dest>/arms/arm-<id>.html`. Clear `<dest>/arms/` first on every pass through this step, including from Step 5's loop, so a file left behind by a wider earlier round never survives into a narrower one.
|
|
27
27
|
|
|
28
|
-
- One page, never
|
|
28
|
+
- One page for the pick, never a set of separate images handed to the operator to compare from memory. The comparison they judge is `candidates.html`, which Step 3 renders and Step 4 asks about. The per-arm files exist only for Step 6's archival capture, once the pick is made, and only the last pass through this step is what Step 6 finds there.
|
|
29
|
+
- Wrap each arm's markup in the same class on both files, chosen once per run and reused everywhere, so one selector addresses an arm on the combined page and on its own standalone file alike.
|
|
29
30
|
- Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
|
|
30
31
|
- Inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
|
|
31
32
|
- Declare a font stack the machine resolves, such as `system-ui` behind a generic fallback. The render refuses a page that would rewrap against a substitute rather than shipping a false comparison, so a page naming no font at all is refused on whatever the default resolves to.
|
|
@@ -65,10 +66,13 @@ Put the choice to the operator through the structured question surface, per `.cl
|
|
|
65
66
|
## Step 6: close
|
|
66
67
|
|
|
67
68
|
1. Apply the winning arm to the real surface, in one change.
|
|
68
|
-
2. Close out whatever document stated the decision as open, in the same change, naming the arm that won and the ones that stayed defensible. A pick that changes a surface and records nothing about why leaves the next reader to re-derive it from a diff
|
|
69
|
-
3.
|
|
70
|
-
4.
|
|
71
|
-
5.
|
|
69
|
+
2. Close out whatever document stated the decision as open, in the same change, naming the arm that won and the ones that stayed defensible. A pick that changes a surface and records nothing about why leaves the next reader to re-derive it from a diff. Skip this where nothing stated the decision.
|
|
70
|
+
3. Batch-capture the final round's arm files, when `<dest>` is the scratch path: `canon capture <dest>/arms --selector <wrapper-class> --out <archive-dir>`, naming Step 2's chosen class. This is the directory-batch convention `draft-identity` Step 6 already uses.
|
|
71
|
+
4. Resolve `<archive-dir>` as `.canon/review/evidence/<slug>/` against the main worktree root, per `.claude/rules/canon/core/085-worktrees.md`, never against a linked worktree this run happens to be building in. The capture is what keeps every arm past the pick, the losing ones included, as a durable revert record distinct from the live comparison page.
|
|
72
|
+
5. Delete `<dest>` and every file inside it, when `<dest>` is the scratch path, now that every arm sits at the durable path above. A variant left behind there is a second design nobody maintains.
|
|
73
|
+
6. Leave `<dest>` in place when it is a live track's `evidence/<slug>/`: `plan-groundwork`'s write scope treats evidence as durable rather than as scratch a session may delete, and the arms already sit at a durable path there.
|
|
74
|
+
7. Report `<dest>` as still standing when the scratch-path delete is refused, naming the path for the operator to remove, rather than closing on a report the tree contradicts. The pick is applied either way, so the run has done its work and the folder is what outlives it.
|
|
75
|
+
8. Report every surface that changed, each on its own line, name the arm that won by its id and its cost, and report the archival path from Step 3 where it ran.
|
|
72
76
|
|
|
73
77
|
## Reading a measurement
|
|
74
78
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: identity
|
|
2
|
+
name: draft-identity
|
|
3
3
|
description: Why the mark and the card are one pick rather than two, and where the size sequence and the write folder come from before either is drafted
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Draft identity requirement
|
|
7
7
|
|
|
8
8
|
## Gap
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: identity
|
|
2
|
+
name: draft-identity
|
|
3
3
|
description: Drafts a project's logo mark through draft-and-pick's render-and-pick loop, then composes the picked mark into an icon sequence and a 1200x630 social card. Use when asked to "make a logo", "design a logo mark", "create a favicon", "build the icon set", "generate a social card", "make an og:image", or "draft the logo and social card together". Do NOT use to mutate an existing logo file directly, which is a plain edit, or to record the mark's construction rules in DESIGN.md, which is out of scope.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Draft identity
|
|
7
7
|
|
|
8
8
|
One identity rendered twice: the same mark sized down to an icon sequence and composed with type into the social card sized up. Drafting both from one pick keeps the mark and its card composition from settling on shapes that do not match.
|
|
9
9
|
|
|
@@ -27,13 +27,24 @@ Run `git diff --cached --name-only 2>/dev/null` to check for staged files. If ou
|
|
|
27
27
|
2. Invoke `canon:docs-fold` to sync internal planning docs against session decisions, folding in the routed facts
|
|
28
28
|
3. Invoke `canon:docs-sync` to sync public docs against changes since main
|
|
29
29
|
4. Run `git add -A` to stage any files the sync skills wrote
|
|
30
|
-
5.
|
|
31
|
-
6. Invoke `canon:git-
|
|
32
|
-
7. Invoke `canon:git-
|
|
33
|
-
8.
|
|
34
|
-
9.
|
|
30
|
+
5. Run `canon tasks plan-reach <plan> --json` and report both lists it carries. Name the plan this branch built under, by path or by slug. Read `claimed` first and say who holds each path, since that is the half a reader acts on, then say how many of the changed paths `undeclared` names. Branch on the record rather than on the exit code, which a shell function wrapping `canon` can flatten to zero. This step reports and never stops the sequence.
|
|
31
|
+
6. Invoke `canon:git-stage` to group staged changes and commit by concern
|
|
32
|
+
7. Invoke `canon:git-branch` to rename branch to match conventional format
|
|
33
|
+
8. Invoke `canon:git-pr` to push branch and open pull request
|
|
34
|
+
9. After the PR opens, watch CI. Poll `canon pr checks <number> --json` until the record's `state` leaves `pending`, branching on that field rather than on the exit, and fall back to `gh pr checks <number>` when no record comes back at all, which is a target whose CLI predates the verb. On `passing`, continue. On `failing`, stop the sequence and report the failing check with its URL. Do not auto-fix. This step may output on failure, the one exception to the no-text-between-steps rule.
|
|
35
|
+
10. If step 1 wrote or updated at least one memory file, invoke `canon:memory-review` scoped to those entries to propose fixes while session context is fresh. If the pen got nothing, skip this step.
|
|
35
36
|
|
|
36
|
-
A caller wrapping this sequence may act between step
|
|
37
|
+
A caller wrapping this sequence may act between step 8 and step 9, which is the one gap the order leaves open, since the pull request exists there and nothing has read its checks yet. `auto-ship` marks the pull request draft in it. Nothing else may go there, and a caller that needs a step anywhere else in the sequence is asking for a change to this body rather than for a place to stand.
|
|
38
|
+
|
|
39
|
+
### Why the reach reads at step 5
|
|
40
|
+
|
|
41
|
+
The branch is whole there and nowhere earlier. Steps 2 and 3 write past whatever the plan declared, so a reading taken ahead of them misses the chain's own additions, and no pull request exists until step 8 to carry the answer anywhere.
|
|
42
|
+
|
|
43
|
+
It is the one step here that reports rather than acts, which is why it never stops the sequence. Every crossing measured on the wave it was filed against merged clean, and the undeclared list runs long enough on an ordinary branch that a gate would fire on nearly every ship.
|
|
44
|
+
|
|
45
|
+
### When the verb is absent
|
|
46
|
+
|
|
47
|
+
An installed binary carrying no `plan-reach` subcommand reports the reach unread rather than clear. Say that, and continue. The verb ships with the CLI and this body ships with the plugin, so a target on an older binary meets a missing subcommand, and a body reading that absence as a clean answer would report the check passing on every branch that never ran it.
|
|
37
48
|
|
|
38
49
|
Capture leads the sequence because a routed fact lands in a context entry, which is a tracked file. Running it after the pull request opens leaves that edit off the branch entirely, so the fact reaches nothing. Memory files are gitignored either way, which is what hid the ordering while capture wrote only those.
|
|
39
50
|
|
|
@@ -69,6 +69,8 @@ Take the comparison at the file path rather than at a folder above it. `canon ta
|
|
|
69
69
|
|
|
70
70
|
The finding names which row contributed the containing path, and a bare-folder cell reports as a claim of its own beside the findings. Read that output as a candidate list, settle each pair by file, and narrow the cell that over-claimed rather than discounting the collision it caused.
|
|
71
71
|
|
|
72
|
+
A declared set is what a branch sets out to write rather than a bound on it, so this gate clears against a prediction and the branch outgrows it hours later. Three tracks crossed a set they had cleared against in the wave of 2026-09-08 and every one merged clean, which is a gate reporting disjoint while a real overlap stood. Two classes account for nearly all of it. The ship chain's own steps write past every plan, since `canon:docs-fold` refreshes whichever context entry a change reaches and `canon:docs-sync` reaches the public docs, and neither surface is one a planner can name before the change exists. The drift stages `bun run check` regenerates and asserts are the second, together with the test and sandbox siblings a source change drags in. Nothing here prevents either, because the files at issue are written long after this gate clears. What reads the other end is `canon tasks plan-reach`, which `canon:git-ship` runs at step 5 against the branch's own diff, so a crossing this gate could not see is named before the pull request opens rather than after it merges.
|
|
73
|
+
|
|
72
74
|
Disjointness is necessary and not sufficient, so hold a candidate whose sets do not touch when a stated reason serializes it, and write the reason on the hold. One row creating a skill and another auditing that catalog and counting it write nothing in common, measured 2026-08-27, and dispatching both still leaves the audit counting a denominator that moves underneath it. Nothing verifies that a reason was written, so the rule holds only while the dispatcher applies it.
|
|
73
75
|
|
|
74
76
|
What binds past that is review attention rather than a count, and `## Parallelism` in the skill body states it along with the cap an operator can set for a session. The one number this skill carries is the review fallback's count of three in `## Parallelism`, which moves a review rather than binding a track, and this runbook carries none.
|
|
@@ -84,6 +84,11 @@ candidates and stop where more than one does, rather than addressing the first.
|
|
|
84
84
|
Send to the single row where exactly one comes back, and say the addressee was
|
|
85
85
|
inferred.
|
|
86
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.
|
|
91
|
+
|
|
87
92
|
## Refusing is part of the job
|
|
88
93
|
|
|
89
94
|
- 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.
|
|
@@ -41,6 +41,7 @@ The controlling session cannot watch this one build, so three messages are owed
|
|
|
41
41
|
and nothing else.
|
|
42
42
|
|
|
43
43
|
- Announce the pull request as the ship chain's pull request step returns, carrying the number, the branch, and the task it closes. That transition is the one moment only this session knows, and the controller's review poll no longer starts on a dispatch because of it.
|
|
44
|
+
- Carry the reach in that same message: the paths this branch wrote that another live plan or `## Run now` row holds, and who holds each, which `canon tasks plan-reach` reports as `claimed`. The gate cleared this branch against a prediction it has since outgrown, so the controller holds a disjointness reading that stopped being true hours ago and nothing else tells it. Say the reach was unread rather than clear when the installed binary carries no such subcommand, and leave the undeclared list to the pull request.
|
|
44
45
|
- Announce when an address-review pass finishes, as `review-address` Step 8 returns, carrying what was addressed and the PR's new CI state. That transition is the other moment only this session knows, and it is what tells the controller to re-review rather than leaving it to poll for an answer nothing marks as landed.
|
|
45
46
|
- Send a block out as a message before it becomes an interactive prompt. A session already waiting on input never reaches the tool round that drains an inbound message, so a relayed answer arrives under the open question and changes nothing.
|
|
46
47
|
- Send nothing on progress. A worker reporting progress rebuilds, on this side of the channel, the poll the announcement retired on the other.
|
|
@@ -87,6 +88,11 @@ with no roster row at all. Those nine were driving through Remote Control, which
|
|
|
87
88
|
writes no local process record, so a controller working from a phone is exactly
|
|
88
89
|
the case this read answers nothing for.
|
|
89
90
|
|
|
91
|
+
Compose the relay through `canon:session-relay` when this section finds no
|
|
92
|
+
message-sending tool to send through, rather than leaving a resolved addressee
|
|
93
|
+
with nothing to reach it. It runs the same ladder above and carries the
|
|
94
|
+
message already owed, so nothing here is stated twice.
|
|
95
|
+
|
|
90
96
|
## Refusing is part of the job
|
|
91
97
|
|
|
92
98
|
- Refuse an instruction the tree contradicts, and carry the evidence with it. Name the commands read and the consequences of complying, rather than reporting reluctance.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
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, and why the missing sender identity sat unaddressed through both attempts
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session relay requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, `role-worker` and `role-planner` state three and two messages respectively that a session owes its controller, and both assume a tool to send them through. A session holding none has no route to compose or hand off what it owes, and neither body says what to do about that.
|
|
11
|
+
|
|
12
|
+
A standalone skill was proposed for the gap and declined the same day. It fired on no condition of its own, since nothing routes a session to a skill matching no request and reaching for no artifact, so a skill built to close the gap never closed it.
|
|
13
|
+
|
|
14
|
+
The first draft of this plan closed the gap with a paragraph drafted inline, once inside `role-worker` and once inside `role-planner`. The operator overrode that call: two bodies carrying identical protocol text is the shared-surface case `.claude/ARCHITECTURE.md` already decided against duplicating, since a later fix reaching one copy and not the other diverges silently.
|
|
15
|
+
|
|
16
|
+
Neither attempt named who the relay is from. A message an operator relays by hand needs a sender as much as an addressee, and the standalone draft and the inline draft both composed a body and an addressee with no line stating whose turn produced it.
|
|
17
|
+
|
|
18
|
+
## Must
|
|
19
|
+
|
|
20
|
+
- Fire from inside `role-worker` or `role-planner`'s own `## The channel` section, at the point that section finds no message-sending tool
|
|
21
|
+
- 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
|
|
22
|
+
- Resolve the addressee by running the calling role's own ladder rather than a second one
|
|
23
|
+
- Carry the message body the calling role's bullet already names, verbatim
|
|
24
|
+
- Open the composed text with a header naming the sender, the addressee, and what the message is
|
|
25
|
+
- State the header as a default a session may write around, rather than a fixed vocabulary
|
|
26
|
+
|
|
27
|
+
## Must not
|
|
28
|
+
|
|
29
|
+
- Restate the addressee ladder `role-worker` or `role-planner` already states
|
|
30
|
+
- Draft a message body of its own. The text it composes belongs to the calling role's bullet.
|
|
31
|
+
- Fire when a message-sending tool exists
|
|
32
|
+
|
|
33
|
+
## Guards
|
|
34
|
+
|
|
35
|
+
- A message-sending tool is available: refuse, name it, and send through it instead
|
|
36
|
+
|
|
37
|
+
## Out of scope
|
|
38
|
+
|
|
39
|
+
- The addressee resolution ladder itself, which stays in `role-worker` and `role-planner`
|
|
40
|
+
- The content of each owed message, which stays in the calling role's own bullet
|
|
41
|
+
- Inbound reach: a controller relaying back to a worker or planner holding no tool. The gap this closes is the outbound leg alone, since a controller can already reach either side directly.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-relay
|
|
3
|
+
description: Composes the relay a worker or planner owes its controller when the session holds no message-sending tool, carrying the sender's own name, the addressee the calling role's own ladder resolves, and the message body that role already names, opened with a header naming all three so an operator can copy the block whole. Fires only from inside `role-worker` or `role-planner`'s own `## The channel` section, at the point that section finds no tool to send through. Do NOT invoke it when a message-sending tool exists, which is the ordinary channel, and do NOT invoke it to resolve an addressee for anything other than a channel obligation those two roles already state.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session relay
|
|
7
|
+
|
|
8
|
+
`role-worker` and `role-planner` each owe their controller a set of messages, and each assumes a message-sending tool exists to carry them. A session holding none has nothing to send through, and the paragraph stating what to do about that used to sit inline in both bodies, repeating the same protocol twice. This skill is that paragraph, and both role bodies point to it instead of each carrying their own copy.
|
|
9
|
+
|
|
10
|
+
Read the calling body's `## The channel` section for the message owed and the ladder that resolves its addressee. This skill does not restate either.
|
|
11
|
+
|
|
12
|
+
## Guards
|
|
13
|
+
|
|
14
|
+
- Refuse to fire when a message-sending tool is available. Say so, name the tool, and send through it. This skill exists for the gap, never as an alternative to the ordinary channel.
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
1. Read this session's own name from `canon sessions list --self --json`. A refusal here, `no-self-identity` or `no-self-row`, means the roster carries nothing to name the sender with. Report the refusal and its reason in place of a name rather than guessing one.
|
|
19
|
+
2. Resolve the addressee. Run the calling role's own `## The channel` ladder to the letter rather than a second one here, and take its outcome, whatever it is, as this skill's addressee.
|
|
20
|
+
3. Carry the message body the calling role's bullet already names, verbatim. This skill composes and never drafts, so the pull request announcement, the address-review announcement, or the blocked question is exactly the text that bullet states, with its own facts filled in.
|
|
21
|
+
4. Open the composed text with a header naming the sender, the addressee, and what the message is. Close the turn with nothing after it, so the block above is the whole of what an operator copies.
|
|
22
|
+
|
|
23
|
+
Where the installed CLI answers `--self` with an unknown option, that flag is newer than the release this session holds, so step 1 meets no refusal with a reason to report. Name the sender from what the environment states instead, and say the roster did not answer, so the reader knows the name was not confirmed against it.
|
|
24
|
+
|
|
25
|
+
## Header
|
|
26
|
+
|
|
27
|
+
```plaintext
|
|
28
|
+
Relay: from <sender> to <addressee>, <what the message is>
|
|
29
|
+
<message body>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
States a default rather than a fixed vocabulary. A session may write the header in its own words, since it exists to be read once and copied rather than parsed by anything downstream. Name the same three parts however phrased.
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
Nothing beyond the composed block. Report the Guards refusal instead when the roster cannot answer for the sender, and report the calling body's own halt or inference when its ladder cannot resolve the addressee.
|
|
@@ -18,6 +18,7 @@ Read these in parallel from the project root, skipping any that do not exist:
|
|
|
18
18
|
- `.claude/DESIGN.md`: visual intent, tokens, typography, spacing rules
|
|
19
19
|
- `.claude/wireframes/index.md` + every `.claude/wireframes/<surface>.md`, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists: intended UI layout, UI copy, interaction rules. Surfaces are loaded one file at a time during Step 3. Per `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`, these carry layout and intent, not implementation detail.
|
|
20
20
|
- `.claude/REQUIREMENTS.md`: feature scope and non-goals
|
|
21
|
+
- A committed capture beside the surface's own source, when the project has one. A rendered surface reads differently composed than it reads written, and a capture is the only artifact here that shows the composed result rather than the markup. Absent one, note that this audit reads source only, and keep going. This is a stated fallback, not a stop: a project with no capture yet, or a capture mid-rebuild, still gets a source-only audit rather than losing the skill entirely.
|
|
21
22
|
|
|
22
23
|
## Step 2: identify surfaces
|
|
23
24
|
|
|
@@ -34,6 +35,8 @@ For each surface, look for:
|
|
|
34
35
|
|
|
35
36
|
Use `.claude/DESIGN.md` and the per-surface `.claude/wireframes/<surface>.md` files, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists, as ground truth for intent. For each implementation surface, read the matching wireframe file before flagging drift. Observations only, no implementation suggestions or fixes.
|
|
36
37
|
|
|
38
|
+
Where Step 1 found a committed capture for this surface, judge composed output (spacing, overlap, contrast, wrapping) against it rather than inferring layout from markup alone, which reports what the surface renders rather than what its source implies. Where none exists, judge from source alone and say so in that surface's findings, since a source-only read cannot see what only a render would show.
|
|
39
|
+
|
|
37
40
|
## Step 4: report and persist
|
|
38
41
|
|
|
39
42
|
### Report format
|
|
@@ -37,7 +37,7 @@ They are reported apart because they want different tolerances.
|
|
|
37
37
|
- **`unmet`** is a whole path the body claims ahead of its bullet's first comma and the diff does not carry. This is the graded direction and it sets the exit code. A bullet naming an untouched file is wrong more often than not, and it corrupts the record that reaches the trunk.
|
|
38
38
|
- **`unnamed`** is a changed file no bullet reached that a reader might have wanted one for. Reported with no grade, since a change can be too small to describe and still be correctly absent. Grading it would fire on nearly every branch.
|
|
39
39
|
- **`incidental`** is a changed file no bullet reached that owes none: a test beside its subject, anything under a fixture or snapshot folder, and a lockfile a package manager writes. Held apart so the count above reads, and reported rather than dropped so a run still says what it set aside.
|
|
40
|
-
- **`unresolved`** is a path the reading could not judge either way.
|
|
40
|
+
- **`unresolved`** is a path the reading could not judge either way. Three causes reach it. A path written partially, such as `role-worker/SKILL.md` for a file under `claude/skills/`. A path past its bullet's first comma. And, for a live pull request, a claim landing here rather than `unmet` because the rename or `.gitignore`-addition evidence that might have credited it could not be read, which `evidenceUnread` on the record names.
|
|
41
41
|
|
|
42
42
|
Each direction splits on one question, which is whether the evidence is strong enough to raise with a person. A partial path and a trailing path can each credit a changed file and can never accuse one, because nothing separates a path written short from a path written wrong, or a second claim from a file cited for context. Neither split drops anything: what comes out of `unmet` lands in `unresolved` and what comes out of `unnamed` lands in `incidental`, so a count a reader can act on never costs a file the run stayed silent about.
|
|
43
43
|
|
|
@@ -105,6 +105,10 @@ The bound is that the corpus is one repository writing to one house style. A pro
|
|
|
105
105
|
|
|
106
106
|
What it cannot see is a bullet claiming a change to a file the branch did touch for another reason, and a path written without backticks. A claim in a second coordinated clause after the comma is read and credited, and it stays out of the graded direction, since nothing here separates it from a file the same clause cites for context.
|
|
107
107
|
|
|
108
|
+
Two shapes credit against evidence the changed-file list itself never carries. A bullet naming the path a git rename moved away from, such as "Move `claude/skills/identity/` to `claude/skills/draft-identity/`", is credited against a local rename read (`git diff --name-status -M`) or, for a live pull request, `gh api repos/{owner}/{repo}/pulls/{number}/files`, keyed on the old path git's own detection reports. A bullet naming a pattern newly added to `.gitignore`, such as "Ignore `web/screenshots/` and `web/evidence/`", is credited against the added, non-comment lines of that file's own diff, with the trailing slash a directory claim carries normalized against the raw pattern. Both credits reach past the anchored-and-leading split that decides `unmet`, and the ignore-addition credit marks `.gitignore` itself as named, so the file also stops reading as `unnamed`.
|
|
109
|
+
|
|
110
|
+
Neither credit can tell a claim that is genuinely stale from one the evidence would have credited had the read gone through, since a failed fetch and a confirmed absence both produce the same empty result. `evidenceUnread` on the record separates the two: when the read that could have credited a claim fails, that claim lands in `unresolved` instead of `unmet`, rather than risk the exact false accusation this section exists to remove.
|
|
111
|
+
|
|
108
112
|
One class stays open and is named rather than closed. A bullet can cite where something is defined while claiming an edit somewhere else, as `erclx/canon#1274` does with "Name the slug transform in `standards/slug.md` inline in the same step". The path is a definition site, the edit target is the step, and separating the two needs the sentence parsed rather than cut. A finding on such a bullet is this class rather than a stale claim, and a reader weighing an `unmet` path checks whether the bullet's real target is a locative the path does not name. <!-- canon-allow-reference: illustrates the input shape the rule reads -->
|
|
109
113
|
|
|
110
114
|
## Where it runs
|
package/docs/agents/tasks.md
CHANGED
|
@@ -186,6 +186,39 @@ Both sides of a dispatch call it. The orchestrator's collision check derives its
|
|
|
186
186
|
canon tasks plan-branch dispatch-answer-gate --json | jq -r '.branch'
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
## Plan reach
|
|
190
|
+
|
|
191
|
+
`canon tasks plan-reach <plan>` reads a branch back against what was written down about it. It reports and never writes, and it names the plan the same two ways `canon tasks plan-branch` does.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
canon tasks plan-reach dispatch-answer-gate
|
|
195
|
+
canon tasks plan-reach dispatch-answer-gate --base origin/main --json
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Option | Effect |
|
|
199
|
+
| --------------- | ---------------------------------------------- |
|
|
200
|
+
| `--base <ref>` | Far side of the range, defaulting to the trunk |
|
|
201
|
+
| `--json` | Emit a machine-readable record on stdout |
|
|
202
|
+
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
203
|
+
|
|
204
|
+
The record carries `claimed`, `undeclared`, `declared`, `base`, `changed`, `plans`, `rows`, and `board`. Exit codes: `0` read with nothing claimed, `1` refused with `no-plan`, `archived`, `bad-input`, `no-base`, or `no-diff`, `2` read with a claim standing. Branch on the record rather than on the exit code, which a shell function wrapping `canon` can flatten to zero.
|
|
205
|
+
|
|
206
|
+
`claimed` leads because it is the short list and the one worth acting on. It carries one entry per path, each holding a `holders` list, so a track carrying both a live plan and a `## Run now` row reads as one holder rather than two. A holder names itself, its `source` of `plan` or `row`, and the `declaration` it matched on, so a folder claim reports which folder rather than leaving the reader to find it. A plan holder also carries `rowed`, whether that plan has a row in `## Run now` at all: a plan with no row is the shape a plan nobody archived takes, and equally the shape of one whose task has yet to be dispatched, so it narrows the reader's search rather than answering it. `undeclared` is every changed path this plan never named, which runs long on an ordinary branch: over the wave this verb was filed against, it ran 22 of 26 paths on one pull request and 18 of 25 on another. Those are the ship chain's own writes rather than scope creep, since the sync skills reach a context entry and the public docs, and the check stages regenerate what they assert.
|
|
207
|
+
|
|
208
|
+
A declaration is a backticked span standing as an entry's subject, ahead of the colon opening its reason. Reading every span was the alternative and it reports pairs that were never going to collide, since a reason routinely cites a file the entry is not about: measured over the same wave, the subject rule reports 6 crossing pairs against 14 for every span. Both sides of a rename declare, since both sit ahead of the colon.
|
|
209
|
+
|
|
210
|
+
The range is read at the current directory and the plans and board at the board root, so a linked worktree reads its own branch against the shared records. Reading both at one root was the alternative and it measures a main checkout sitting on the trunk, where the range closes on itself and every branch reports a reach of nothing.
|
|
211
|
+
|
|
212
|
+
It reads only what is written down, so it inherits the dispatch runbook's blindness: a hand-launched track carries no row and a track with no plan carries no declaration. The `plans`, `rows`, and `board` fields say how much there was to compare against, so a clear reading over an empty corpus does not read as a proof. A missing board reports `board: false` and zero rows rather than refusing, since a project with plans and no board still has a branch worth reading.
|
|
213
|
+
|
|
214
|
+
A claim is only as current as the folder it was read from, and the live folder holds a plan whose work already shipped until something archives it. `canon tasks archive` moves a plan on merge, so a plan stranded by a run that never reached the archive keeps claiming its files against every branch afterwards. Check whether the holder is actually in flight before treating a claim as a collision: the first run of this verb on its own branch reported five paths held by a plan whose verb had already merged, and the whole reading came of a file nobody archived. `canon tasks validate` is what reports the stranded plan itself.
|
|
215
|
+
|
|
216
|
+
`canon:git-ship` runs it at step 5, after `git add -A` and before the commit grouping. That is the first point the branch is whole and the last before a pull request exists to carry the answer.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
canon tasks plan-reach dispatch-answer-gate --json | jq -r '.claimed[] | "\(.path) held by \([.holders[].name] | join(", "))"'
|
|
220
|
+
```
|
|
221
|
+
|
|
189
222
|
## Plan link
|
|
190
223
|
|
|
191
224
|
`canon tasks plan-link <task> <plan>` writes or corrects a task's `Plan:` line, as `Plan: [<label>](<target>)` right after the H1. `plan-feature` calls it right after a plan file lands, when Step 1 resolved an existing task for the feature, so the line is a mechanical write rather than hand-edited markdown.
|
|
@@ -271,6 +271,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
271
271
|
| `canon:role-orchestrator` | To assert the control session that owns the queue and reviews each worker's PR |
|
|
272
272
|
| `canon:role-planner` | To assert the planner role for a cold session writing one plan under one task |
|
|
273
273
|
| `canon:role-worker` | To assert the worker role for a cold session building one branch under one plan |
|
|
274
|
+
| `canon:session-relay` | When a worker or planner owes its controller a message and holds no send tool |
|
|
274
275
|
| `canon:session-resume` | At the start of a session, to pick up what a previous one left |
|
|
275
276
|
| `canon:session-map` | At the close of a session, to write the handoff a compaction would destroy |
|
|
276
277
|
|
|
@@ -305,7 +306,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
305
306
|
| `canon:draft-slides` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
306
307
|
| `canon:draft-screencast` | For a recording script with beats and defaults already seeded |
|
|
307
308
|
| `canon:record-screencast` | For compiling and running a screencast draft into a recording and a still |
|
|
308
|
-
| `canon:identity`
|
|
309
|
+
| `canon:draft-identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
|
|
309
310
|
|
|
310
311
|
### Answer a question at any point
|
|
311
312
|
|
|
@@ -40,7 +40,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
40
40
|
- `canon:ux-audit` for UX gap detection on existing surfaces
|
|
41
41
|
- `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
42
42
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
43
|
-
- `canon: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
|
|
43
|
+
- `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
|
|
44
44
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
45
45
|
|
|
46
46
|
### When to pick
|
|
@@ -34,5 +34,6 @@ paths:
|
|
|
34
34
|
|
|
35
35
|
- Do not commit the sweep. It stays ignored.
|
|
36
36
|
- Commit a flagged case's evidence output so the pull request carries the comparison, rather than attaching it by hand.
|
|
37
|
+
- A project whose own established convention already commits that same comparison may route a flagged case's evidence there instead of a separate evidence folder. Only an established convention already committing the same comparison qualifies, not a preference for skipping a second folder.
|
|
37
38
|
- Commit an evidence case for the first time only after running the capture twice with no code change between the runs and confirming the two outputs are byte-identical.
|
|
38
39
|
- Recommitting an unbounded sweep on every run reaches a gigabyte of repository history inside a hundred merges. A small, committed evidence set is what a reviewer needs and what lets GitHub draw its own before-and-after comparison on the pull request.
|
package/package.json
CHANGED
package/src/claude/cases/misc.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
15
15
|
{
|
|
16
16
|
prompt:
|
|
17
17
|
'This project has no logo yet. Draft one and give me a social card to go with it.',
|
|
18
|
-
expect: 'identity',
|
|
18
|
+
expect: 'draft-identity',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
prompt: 'Fire up the dev server the way this project documents it.',
|
|
@@ -47,4 +47,8 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
47
47
|
prompt: 'Is there a page anywhere in this repo that covers retries?',
|
|
48
48
|
expect: 'index-lookup',
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
prompt: 'Connect this repo to Cloudflare Pages and set up the deploy.',
|
|
52
|
+
expect: 'deploy-cloudflare',
|
|
53
|
+
},
|
|
50
54
|
]
|
|
@@ -142,6 +142,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
142
142
|
prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
|
|
143
143
|
expect: 'session-worktree',
|
|
144
144
|
},
|
|
145
|
+
{
|
|
146
|
+
prompt:
|
|
147
|
+
'I owe my controller the pull request announcement but I have no message-sending tool. Compose the relay so an operator can copy it over.',
|
|
148
|
+
expect: 'session-relay',
|
|
149
|
+
},
|
|
145
150
|
|
|
146
151
|
// Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
|
|
147
152
|
{
|