@erclx/canon 5.40.0 → 5.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +34 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/create-skill/references/skill.md +1 -1
  7. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  8. package/claude/skills/decision-escalate/SKILL.md +1 -2
  9. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  10. package/claude/skills/git-branch/SKILL.md +63 -7
  11. package/claude/skills/plan-feature/references/batches.md +48 -1
  12. package/claude/skills/plan-feature/references/plan.md +2 -0
  13. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  14. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  15. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  16. package/claude/skills/review-ui/SKILL.md +2 -0
  17. package/claude/skills/review-ui/references/close-out.md +47 -0
  18. package/claude/skills/review-ui/references/post.md +3 -1
  19. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  20. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  21. package/claude/skills/role-worker/SKILL.md +9 -0
  22. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  23. package/claude/skills/teach-workspace/SKILL.md +1 -2
  24. package/docs/workflow/parallel-features.md +1 -0
  25. package/docs/workflow/skill-map.md +1 -1
  26. package/package.json +1 -1
  27. package/src/claude/cases/git.ts +4 -0
  28. package/src/commands/pr.ts +32 -9
  29. package/src/commands/records.ts +11 -3
  30. package/src/context/shape-gated.ts +1 -0
  31. package/src/markdown/scope.ts +6 -2
  32. package/src/pr/frames.ts +53 -9
  33. package/src/records/validate.ts +1 -0
  34. package/src/tasks/answers.ts +79 -2
  35. package/src/tasks/fan-in.ts +176 -0
  36. package/src/tasks/stack.ts +1 -1
  37. package/tooling/astro/reference.md +80 -39
  38. package/tooling/base/reference.md +63 -32
  39. package/tooling/base/seeds/canon/context/ci.md +7 -15
  40. package/tooling/base/seeds/canon/context/development.md +14 -14
  41. package/tooling/claude/reference.md +40 -22
  42. package/tooling/cloudflare/reference.md +31 -12
  43. package/tooling/go/reference.md +47 -23
  44. package/tooling/nextjs/reference.md +52 -19
  45. package/tooling/php/reference.md +35 -16
  46. package/tooling/python/reference.md +47 -30
  47. package/tooling/shell/reference.md +6 -5
  48. package/tooling/vite-react/reference.md +49 -21
  49. package/tooling/web/reference.md +130 -70
package/README.md CHANGED
@@ -12,7 +12,6 @@
12
12
  [![License MIT](https://img.shields.io/npm/l/@erclx/canon)](LICENSE)
13
13
 
14
14
  canon is a CLI and Claude Code plugin that runs your agent's work from plan to merge, holds it to the practices that make code worth keeping, and installs the same setup in every repository.
15
-
16
15
  **[See it run at canon.erclx.dev](https://canon.erclx.dev)**
17
16
 
18
17
  </div>
@@ -21,9 +20,13 @@ That page is one real session the toolkit ran on itself, and every count on it i
21
20
 
22
21
  ## Why
23
22
 
24
- If an agent writes code for you in more than one repository, this is for you. canon runs that work from plan to merge. A planner writes the plan, a worker builds it on its own branch, and a separate session reviews the pull request. It holds the work to a practice, with rules that load when a matching path is edited and checks that fail when a rule is ignored. Both run on one setup, installed the same way in every repository you keep.
23
+ If an agent writes code for you in more than one repository, this is for you. canon runs that work from plan to merge. A planner writes the plan, a worker builds it on its own branch, and a separate session reviews the pull request.
24
+
25
+ It holds the work to a practice, with rules that load when a matching path is edited and checks that fail when a rule is ignored. Both run on one setup, installed the same way in every repository you keep.
25
26
 
26
- That shared setup is what the rest of this page describes, and it is the part that drifts without a source. Every AI coding setup accumulates the same assets. Prompts to reuse, rules agents should follow, slash commands, skills, seed docs, sync scripts. Once you have enough projects, your copies drift and your agents stop getting consistent signals.
27
+ ### The shared setup
28
+
29
+ That shared setup is what the rest of this page describes, and it drifts without a source. Every AI coding setup accumulates the same assets. Prompts to reuse, rules agents should follow, slash commands, skills, seed docs, sync scripts. Once you have enough projects, your copies drift and your agents stop getting consistent signals.
27
30
 
28
31
  Three design choices shape the toolkit.
29
32
 
@@ -61,17 +64,21 @@ bun install --global @erclx/canon
61
64
 
62
65
  Each domain has a canonical source in this repo and a thin install or sync CLI on your side. The domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
63
66
 
64
- | Domain | What it is | How it reaches you |
65
- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
66
- | Plugin skills | Skills that plan a feature, review a diff, sync the planning docs, and run the ship chain | Loaded live from the plugin, never copied |
67
- | Governance | Coding and authoring rules that load into a Claude session when a matching path is edited | Installed per project by `canon gov install`, refreshed by sync |
68
- | Tooling stacks | Golden configs, seeds, and a reference per framework | Laid down by `canon init`, reconciled by `canon tooling sync` |
69
- | Design system | Ships a `DESIGN.md` intent format that points at the project's stylesheet for values, a skill that drafts one from an existing project or from scratch, and a render command | `canon design render` |
70
- | Slides | A folder of HTML slides per deck, drawn in the project's tokens and rebuilt as editable shapes | `canon slides render` writes PowerPoint |
71
- | Teach | Lessons, sources, and a glossary in one workspace, rendered to a small site | `canon teach` |
72
- | Canvas | Pages of HTML frames on a local pan and zoom surface, each frame drawn with the project's tokens | `canon canvas serve` |
73
- | Transcripts | A YouTube transcript with metadata frontmatter | `canon transcripts <url>` writes it into any repo |
74
- | Sandbox | Scenario scaffolds that provision an isolated project state for verifying each domain flow | `canon sandbox` |
67
+ ### Domain catalog
68
+
69
+ | Domain | What it is | How it reaches you |
70
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
71
+ | Plugin skills | Skills that plan a feature, review a diff, sync the planning docs, and run the ship chain | Loaded live from the plugin, never copied |
72
+ | Governance | Coding and authoring rules that load into a Claude session when a matching path is edited | Installed per project by `canon gov install`, refreshed by sync |
73
+ | Tooling stacks | Golden configs, seeds, and a reference per framework | Laid down by `canon init`, reconciled by `canon tooling sync` |
74
+ | Design system | Ships a `DESIGN.md` intent format pointing at your stylesheet for values, a skill that drafts one, and a render command | `canon design render` |
75
+ | Slides | A folder of HTML slides per deck, drawn in the project's tokens and rebuilt as editable shapes | `canon slides render` writes PowerPoint |
76
+ | Teach | Lessons, sources, and a glossary in one workspace, rendered to a small site | `canon teach` |
77
+ | Canvas | Pages of HTML frames on a local pan and zoom surface, each frame drawn with the project's tokens | `canon canvas serve` |
78
+ | Transcripts | A YouTube transcript with metadata frontmatter | `canon transcripts <url>` writes it into any repo |
79
+ | Sandbox | Scenario scaffolds that provision an isolated project state for verifying each domain flow | `canon sandbox` |
80
+
81
+ ### How domains reach you
75
82
 
76
83
  Those domains reach you in three shapes. A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A plugin skill and the references it carries are never copied, so there is no copy in your repo to drift from this one.
77
84
 
@@ -117,6 +124,8 @@ The first two update the skills, the third updates the CLI, and they move indepe
117
124
 
118
125
  `canon upgrade` reads the package manager off its own install path and reinstalls with that one, so you don't have to remember which put it there. It names what it detected before it runs anything, and it refuses a source checkout rather than reinstalling over your clone.
119
126
 
127
+ ### Checking for drift
128
+
120
129
  You don't have to wait until something breaks to find out you're behind. `canon sync --check` and `canon claude skills drift` both report the installed version against the newest published one, and neither changes its exit code over it, so an offline machine reads unknown rather than red.
121
130
 
122
131
  To stop doing this by hand, turn auto-update on once under `/plugin` in the Marketplaces tab. Confirm what you are running with `canon --version` and `claude plugin list`.
@@ -127,6 +136,8 @@ Working on the toolkit starts from a clone. Running the CLI doesn't, since it in
127
136
 
128
137
  ### Prerequisites
129
138
 
139
+ Bun, Git, and a shell are required, and the GitHub CLI is optional.
140
+
130
141
  - [Bun](https://bun.sh) for the CLI runtime and scripts
131
142
  - [Git](https://git-scm.com) with worktree support
132
143
  - [GitHub CLI](https://cli.github.com) (optional) for ship flows
@@ -141,7 +152,10 @@ bun install
141
152
  bun run bootstrap
142
153
  ```
143
154
 
144
- The script is idempotent, so re-run it after pulling upstream changes without duplicating anything. That also means it leaves an alias block you already have alone rather than refreshing it, so an alias added upstream needs the block deleted and the script re-run. It confirms the install by resolving `canon --help` on the last step. See [zshrc aliases](docs/workflow/zshrc-aliases.md) for what each alias does, how to pick up a new one, and how to opt out of the block.
155
+ The script is idempotent, so re-run it after pulling upstream changes without duplicating anything. It confirms the install by resolving `canon --help` on the last step.
156
+
157
+ - It leaves an alias block you already have alone rather than refreshing it, so an alias added upstream needs the block deleted and the script re-run.
158
+ - [Zshrc aliases](docs/workflow/zshrc-aliases.md) covers what each alias does, how to pick up a new one, and how to opt out of the block.
145
159
 
146
160
  With the CLI linked, scaffold a fresh project.
147
161
 
@@ -151,9 +165,12 @@ git init
151
165
  canon init
152
166
  ```
153
167
 
154
- `canon init` installs base tooling configs, Claude seeds, and governance rules in one pass, and scaffolds a `.claude/wiki/` stub for your project's own reference pages. Governance defaults to the `base` stack, so a bare init lands the workflow and doc-authoring rules in `.claude/rules/`. Each rule names the skill it loads, so no corpus is copied into your project.
168
+ `canon init` installs base tooling configs, Claude seeds, and governance rules in one pass, and scaffolds a `.claude/wiki/` stub for your project's own reference pages.
155
169
 
156
- Pass `--stack <name>` for a language stack such as `node` or `python`, or `--skip governance` to leave rules out. Run `canon tooling list --json` to see the catalog.
170
+ - Governance defaults to the `base` stack, so a bare init lands the workflow and doc-authoring rules in `.claude/rules/`.
171
+ - Each rule names the skill it loads, so no corpus is copied into your project.
172
+ - Pass `--stack <name>` for a language stack such as `node` or `python`, or `--skip governance` to leave rules out.
173
+ - Run `canon tooling list --json` to see the catalog.
157
174
 
158
175
  For the full journey from scaffold through adding a domain later to syncing upstream drift, see [target projects](docs/target/projects.md).
159
176
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.40.0",
4
+ "version": "5.41.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -19,6 +19,7 @@ Without this skill, the run from an approved plan to an open pull request is a c
19
19
  - Open the pull request as a draft before the continuous integration watch begins, since a pull request marked after it is mergeable for the length of the run
20
20
  - Continue past a produced UI checklist rather than stopping on it, and report its unchecked boxes in the closing block, since the draft mark already holds the merge
21
21
  - Prove the draft target's head is the current branch and its state is open before marking it, and stop with nothing marked on a mismatch, since the number is retyped between steps and a draft mark on a release pull request holds that release
22
+ - Stop a fan-in member after its verify and test order with a push and a done message rather than a pull request, and open an integrator's build by merging the member branches, since parallel batches sharing edge files put every open sibling into conflict on each merge
22
23
  - Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
23
24
 
24
25
  ## Must not
@@ -73,6 +73,8 @@ When this run decides against a suggestion, rewrite its line as `overridden at e
73
73
 
74
74
  Load `canon:build-in-slices`, reporting it when it does not resolve, and implement only what the plan describes. Do not expand scope, refactor neighbors, or touch files outside the plan's "Files to touch" list without reason.
75
75
 
76
+ When the plan's `**Constraints:**` carry a `Fans in` bullet, this run is a fan-in integrator, so open this step by merging the member branches, per `${CLAUDE_SKILL_DIR}/references/fan-in.md`.
77
+
76
78
  ## Step 3: verify
77
79
 
78
80
  Run the verify commands defined in `CLAUDE.md` (lint, typecheck, tests). On failure:
@@ -97,6 +99,10 @@ The verb reads git history rather than the working tree, so it catches a violati
97
99
 
98
100
  Never read a missing subcommand as clean. Read `${CLAUDE_SKILL_DIR}/references/verb-absent.md` on meeting one.
99
101
 
102
+ ### A fan-in member stops here
103
+
104
+ When the plan's `**Constraints:**` carry a `Fan-in member of` bullet, push the branch, announce it done, and stop before Step 5. A member opens no pull request, since its integrator ships the whole set. Read `${CLAUDE_SKILL_DIR}/references/fan-in.md` for the push and the message.
105
+
100
106
  ## Step 5: UI checklist (conditional)
101
107
 
102
108
  If the diff touches UI files (JSX, TSX, Vue, Svelte, HTML, or CSS under `src/`), invoke `canon:ui-checklist`, then continue to Step 6 whatever it returns, in every run. The Step 6 draft mark holds the merge, so a stop here protects nothing.
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Fan-in member exit and integrator merge
3
+ description: How auto-ship stops a fan-in member after its push and how an integrator merges the member branches before its own build, so a set of parallel batches ships as one pull request
4
+ ---
5
+
6
+ # Fan-in
7
+
8
+ The session reads this file at one of two points in `auto-ship`. A member reads `## Member exit` after Step 4, and an integrator reads `## Integrator merge` at the opening of Step 2. When to fan in and how a plan declares its side are in `${CLAUDE_SKILL_DIR}/../plan-feature/references/batches.md`.
9
+
10
+ ## Member exit
11
+
12
+ Step 3's verify and Step 4's test order have already run, so the branch is green and committed. The member ends here rather than at a pull request.
13
+
14
+ 1. Push the branch with `git push -u origin <branch>`. The pre-push hook runs the project check, and a member that fails it stops the way Step 3 does.
15
+ 2. Read the head with `git rev-parse HEAD` once the push returns.
16
+ 3. Announce the batch done to the controlling session through `canon:session-relay`, carrying the branch, the head commit, the integrator the plan names, and any row this build turned up.
17
+ 4. Stop. Run no UI checklist, no `git-ship`, and no draft mark.
18
+
19
+ The pushed branch is the done signal the integrator's gate reads, so push once, at the end, and never mid-build. The announcement carries no reach, since members of one set share files by design and the integrator's announcement carries the reach for the whole set.
20
+
21
+ A fix the integrator needs after this point lands on the integrator's branch, since a member has no pull request to fix it on.
22
+
23
+ ## Integrator merge
24
+
25
+ The members are pushed before this run starts, since `canon tasks plan-answers` holds an integrator until every member branch is on origin. Confirm that before merging anything:
26
+
27
+ ```bash
28
+ git fetch origin --quiet
29
+ canon tasks plan-answers <plan> --json
30
+ ```
31
+
32
+ Stop on `launchable: false`, reporting each `open` entry, since a `Fan-in pending` entry names a member that has not pushed and a `Fan-in set` entry names a set whose sides disagree. Then merge each member in the order the `Fans in` bullet lists:
33
+
34
+ ```bash
35
+ git merge --no-ff --no-edit origin/<member branch>
36
+ ```
37
+
38
+ ### Conflicts and the order
39
+
40
+ `--no-ff` keeps one merge commit per member, so `git log --first-parent` shows the reviewer one member at a time. Never squash or fast-forward a member in.
41
+
42
+ - Resolve each conflict by `## Resolving a conflict` in `${CLAUDE_SKILL_DIR}/../review-address/references/rebase-conflicts.md`
43
+ - Stop on a hunk needing a decision with `git merge --abort` in place of the rebase abort that file names
44
+ - Regenerate the indexes once, after the last merge, rather than resolving a generated file by hand at each one
45
+ - Run `git config rerere.enabled true` before the first merge, since a stale integrator rebases with `--rebase-merges` per the same file and that replays each merge without its recorded resolution
46
+
47
+ Continue Step 2 with the integrator plan's own `**Files to touch:**`, then the rest of the chain as an ordinary worker.
48
+
49
+ ### What the pull request carries
50
+
51
+ Say in the pull request body that each member is one merge commit and name the members in merge order. Once the pull request merges, delete each member's branch on origin, its worktree, and its local branch:
52
+
53
+ ```bash
54
+ git push origin --delete <member branch>
55
+ git worktree remove <member worktree>
56
+ git branch -D <member branch>
57
+ ```
58
+
59
+ Read each member's worktree path off `git worktree list --porcelain` by its branch. The integrator holds the member list, so the deletion is its step rather than a sweep's. No member opens a pull request, so `canon worktrees reclaim` never reads a member's branch as merged and reports a leftover worktree as unmerged work on every run.
@@ -104,7 +104,7 @@ allowed-tools: <tools required>
104
104
 
105
105
  - **`name`** (required): kebab-case, matches folder name, no spaces or capitals
106
106
  - **`description`** (required): what it does + when to use it, under 1024 chars, no XML tags
107
- - **`disable-model-invocation: true`**: user-invoked only, Claude will not auto-trigger. Set it on a skill that starts a process the operator owns, never on one reachable by a matched trigger.
107
+ - **`disable-model-invocation: true`**: Claude will not auto-trigger, and the `Skill` tool also refuses the operator's own request made in words. Set it only on a skill run by slash command alone. A skill wanting just the first effect writes `Do NOT auto-trigger` in its description.
108
108
  - **`allowed-tools`**: restrict tool access when the skill is active
109
109
  - **`metadata`**: optional key-value pairs (`author`, `version`, `mcp-server`)
110
110
  - **`metadata.family`** (required on a skill under `claude/skills/`): the skill map group the skill's row sits under, as one key from `families` in `canon claude skills list --json`. A skill under `.claude/skills/` takes none.
@@ -41,4 +41,4 @@ The opposite failure is the one that ships silently. A session that takes a pref
41
41
 
42
42
  - Making the ordinary judgment call, which is a pick plus a one-sentence tradeoff and needs no surface
43
43
  - Writing the decision into a plan, task, or context entry, which each owning standard governs and this skill only routes to
44
- - Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so escalating is the operator's call rather than a description match.
44
+ - Deciding when to fire. The description states `Do NOT auto-trigger`, so escalating is the operator's call rather than a description match.
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: decision-escalate
3
- description: Collects every open decision whose answer turns on the operator's preference, since only they know what they need, puts them as one batch of questions each carrying options and a recommended default, then waits. Use when asked to "escalate this", "ask me the open questions", "batch the open decisions", "what do you need from me", or "stop and ask before you pick". Do NOT use for a judgment call the session can settle, which is a pick with the tradeoff stated in one sentence.
4
- disable-model-invocation: true
3
+ description: Collects every open decision whose answer turns on the operator's preference, since only they know what they need, puts them as one batch of questions each carrying options and a recommended default, then waits. Use when asked to "escalate this", "ask me the open questions", "batch the open decisions", "what do you need from me", or "stop and ask before you pick". Do NOT use for a judgment call the session can settle, which is a pick with the tradeoff stated in one sentence. Do NOT auto-trigger.
5
4
  metadata:
6
5
  family: decide
7
6
  ---
@@ -9,12 +9,17 @@ description: What branch naming is for, the gaps it closes, and where the family
9
9
 
10
10
  Without this skill, a branch name is invented once per session. The type prefix drifts from what the history already carries, the name runs past the width a pull request list renders, and a rename lands on a branch that already exists on the remote, which detaches every open pull request pointing at it.
11
11
 
12
+ A session standing on `main` has no route either. The `git.md` rule sends every git operation through a `git-*` skill, and the only one for branches stopped on `Cannot rename a protected branch`, so the first act of any feature, creating its branch, had no skill to go through.
13
+
12
14
  ## Must
13
15
 
14
16
  - Read the branch format reference before generating, so the type vocabulary and the length limit come from one source
15
- - Derive the name from the commits ahead of main when the invocation carries no description
17
+ - Create the branch on `main` or `master` and rename it anywhere else, so one skill covers both ends of a branch's start
18
+ - Derive the name from the commits ahead of main when a rename carries no description
19
+ - Take a created name from `canon tasks plan-branch` when the invocation names a plan, so the skill and the dispatch gate hold one derivation
16
20
  - Resolve whether the branch exists on the remote before proposing a rename, since the hazard is invisible from local state
17
- - Show the name and its length against the limit before renaming
21
+ - Read the remote itself with `git ls-remote` before creating, since `origin/<name>` sees only the last fetch
22
+ - Show the name and its length against the limit before renaming or creating
18
23
  - Execute immediately after the preview. The tool permission dialog is the confirmation gate.
19
24
 
20
25
  ## Must not
@@ -22,17 +27,23 @@ Without this skill, a branch name is invented once per session. The type prefix
22
27
  - Rename `main` or `master`
23
28
  - Rename when the current name already conforms. The skill is chained, so a conforming name has to pass through rather than churn the branch.
24
29
  - Rename a branch that exists on the remote. Local rename plus force push is not the same operation and it breaks the pull request.
30
+ - Create a branch from anywhere but `main` or `master`, or create one whose name exists locally or on the remote
31
+ - Invent a name when the plan verb refuses, since a name the skill calls non-conforming strands the rename at ship
32
+ - Fetch or stash while creating
25
33
  - Emit anything after the result line
26
34
 
27
35
  ## Guards
28
36
 
29
- - On a protected branch: stop and say the branch is protected
30
- - Name already conforms: stop with a success marker, not a failure, since a chained caller continues past it
31
- - No commits and no description: stop and name both missing inputs
32
- - Branch exists on the remote: stop and route to the surface that preserves the pull request link
37
+ - Create with no plan and no name: stop and name both missing inputs
38
+ - Create with a name breaking the branch standard: stop and name the cap
39
+ - Create with a name existing locally or on the remote: stop and say which
40
+ - Rename name already conforms: stop with a success marker, not a failure, since a chained caller continues past it
41
+ - Rename with no commits and no description: stop and name both missing inputs
42
+ - Rename of a branch existing on the remote: stop and route to the surface that preserves the pull request link
33
43
 
34
44
  ## Out of scope
35
45
 
36
46
  - The commit message, which `git-commit` owns
37
47
  - Pushing and opening the pull request, which `git-pr` owns. That skill reads a conforming name as a precondition and refuses without one, so this skill runs first.
38
48
  - Splitting a branch whose commits span concerns, which `git-split` owns
49
+ - Entering a worktree, which `session-worktree` owns and which names its own branch
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: git-branch
3
- description: Generates and validates conventional branch names. Use for any branch naming or renaming, such as when asked to "name this branch".
3
+ description: Generates and validates conventional branch names, creating the branch from main or master and renaming it anywhere else. Use for any branch naming, creating, or renaming, such as when asked to "name this branch" or "create the branch for this feature".
4
4
  metadata:
5
5
  family: ship
6
6
  ---
@@ -13,7 +13,64 @@ Before generating a branch name, read:
13
13
 
14
14
  Follow it exactly.
15
15
 
16
- ## Context
16
+ Run `git branch --show-current` to pick the mode. On `main` or `master` the skill creates a branch, and anywhere else it renames the current one.
17
+
18
+ ## Create mode
19
+
20
+ ### Name
21
+
22
+ Take the name from the invocation. An argument ending in `.md` or resolving under `.canon/plans/` is a plan, and anything else is a branch name, since a name always carries a `<type>/` segment.
23
+
24
+ - For a plan, run `canon tasks plan-branch <plan> --json` and take the record's `branch`. The verb fixes the type at `feat`, so a plan never yields another type.
25
+ - When the verb refuses or the record carries `conforms: false`, stop and relay its reason. Never invent a name in its place.
26
+ - For a name, check it against the whole Format section of `branch.md`, including the description word bound and the no-repeated-type rule, as the rename guard does.
27
+
28
+ ### Context
29
+
30
+ Run these in parallel once the name is known:
31
+
32
+ - `git show-ref --verify --quiet "refs/heads/<name>" && echo "LOCAL_EXISTS" || echo "LOCAL_FREE"`
33
+ - `git ls-remote --exit-code --heads origin "<name>" 2>/dev/null; echo "REMOTE_EXIT=$?"`
34
+
35
+ Exit 0 on the remote read is a collision. Exit 2, or a repository with no `origin`, is no collision. The check reads the remote itself, never `origin/<name>`, which sees only the last fetch. The skill does not fetch.
36
+
37
+ ### Guards
38
+
39
+ Each guard stops before any ref moves.
40
+
41
+ - If the invocation carries no plan and no name, stop: `❌ No plan or branch name to create from.`
42
+ - If the name breaks `branch.md`, stop and name the cap it broke: `❌ <name> breaks the branch standard: <cap>.`
43
+ - If the name exists locally, stop: `❌ Branch <name> already exists locally.`
44
+ - If the name exists on the remote, stop: `❌ Branch <name> already exists on origin.`
45
+
46
+ ### Preview
47
+
48
+ Output the preview first.
49
+
50
+ - **Current:** <main_or_master>
51
+ - **Creating:** <name>
52
+ - **Length:** <count>/50
53
+ - **Source:** <plan | name>
54
+
55
+ After outputting the preview, execute the final command immediately. Claude Code's tool permission dialog is the confirmation gate. Do not wait for user input.
56
+
57
+ ### Final command
58
+
59
+ The branch starts at the current tip, and uncommitted changes carry onto it. The skill stashes nothing.
60
+
61
+ ```bash
62
+ git switch -c <name>
63
+ ```
64
+
65
+ ### After execution
66
+
67
+ Respond with exactly one line and add no other text:
68
+
69
+ `✅ Created: <name>`
70
+
71
+ ## Rename mode
72
+
73
+ ### Context
17
74
 
18
75
  Run these commands in parallel to gather git context:
19
76
 
@@ -21,14 +78,13 @@ Run these commands in parallel to gather git context:
21
78
  - `git rev-parse --verify "origin/$(git branch --show-current)" 2>/dev/null && echo "EXISTS" || echo "LOCAL_ONLY"`
22
79
  - `git log main..HEAD --oneline 2>/dev/null || echo "NO_COMMITS"`
23
80
 
24
- ## Guards
81
+ ### Guards
82
+
83
+ A conforming name passes through, so a chained caller continues past it.
25
84
 
26
- - If already on main or master, stop: `❌ Cannot rename a protected branch.`
27
85
  - If branch name already follows conventions, stop: `✅ Branch name already follows conventions.`
28
86
  - If no commits and no args provided, stop: `❌ No commits or description to derive a branch name from.`
29
87
 
30
- ## Response format
31
-
32
88
  ### Preview
33
89
 
34
90
  Output the preview first.
@@ -55,7 +111,7 @@ Only output if LOCAL_ONLY:
55
111
  git branch -m <current> <suggested>
56
112
  ```
57
113
 
58
- ## After execution
114
+ ### After execution
59
115
 
60
116
  Respond with exactly one line and add no other text:
61
117
 
@@ -9,7 +9,7 @@ Applies to a feature plan that sequences its work into more than one batch. A de
9
9
 
10
10
  ## One batch, one file
11
11
 
12
- One batch is one pull request, and one pull request is one plan file, named for its own concern rather than a numeric suffix. `canon tasks plan-branch` derives one branch from one plan filename.
12
+ One batch is one pull request, and one pull request is one plan file, named for its own concern rather than a numeric suffix. `canon tasks plan-branch` derives one branch from one plan filename. A fan-in member is the exception, keeping its own file and branch and opening no pull request, per `## Fan-in`.
13
13
 
14
14
  - A `**Batch N**` sub-heading inside one file's `**Files to touch:**` shares that branch across every batch, so the first merge strands the rest
15
15
  - Write a plan opening five batches as five files before the first line of any of them
@@ -44,3 +44,50 @@ Slice a batch by one user-visible path end to end rather than by layer, so each
44
44
  - Sequence the riskiest batch first, so the batch most likely to change the plan is built while every later batch can still move
45
45
  - Sequence a sweep last, even when it is the riskiest. A batch rewriting files earlier batches touched forces every batch behind it into one review anywhere else.
46
46
  - Keep each batch's commits contiguous once the work starts, since a batch interrupted by another cannot be lifted onto its own branch afterward
47
+
48
+ ## Fan-in
49
+
50
+ A fan-in builds batches in parallel and ships them as one pull request. Each member builds on its own branch and opens none, and one integrator merges every member branch, resolves the shared lines once, and opens the pull request. The trade is one large review, read one merge commit at a time, in place of a review and a rebase per batch.
51
+
52
+ ### When to fan in
53
+
54
+ Fan in when the split was made for speed rather than meaning. Every one of these holds:
55
+
56
+ - One task with one outcome, so no batch is a feature on its own
57
+ - The batches share edge files, such as a gated-paths list or an index, so separate pull requests rebase each other on every merge
58
+ - No batch needs to ship, be reviewed, or be reverted alone
59
+
60
+ Keep separate pull requests when the pieces are independent features, when one needs its own review or revert, when one may stall and hold the rest, or when the combined diff is too large to review even one merge commit at a time. Batches that build on each other stack or fold, per `## Dependent batches`, since a member stacks on nothing.
61
+
62
+ ### Declaring the set
63
+
64
+ Each side names the other in its `**Constraints:**`, and `canon records validate plans` reports `fan-in-unmatched` wherever the two disagree.
65
+
66
+ - A member writes ``Fan-in member of `feature-<integrator>` ``, naming one integrator
67
+ - The integrator writes ``Fans in `feature-<a>`, `feature-<b>`, and `feature-<c>` ``, in the order it merges them
68
+ - The task's `Plan:` line cites every member and the integrator, since the merge archives only the plans that line cites and a live integrator listing archived members reports `fan-in-unmatched`
69
+ - The integrator is its own plan, since its writes, being the shared lists, the citer repoints, and the index regeneration, need a `**Files to touch:**` of their own
70
+
71
+ `canon tasks plan-answers` holds a member whose integrator is missing or does not list it with a `Fan-in set` entry. It holds the integrator with a `Fan-in pending` entry until every member branch is on origin.
72
+
73
+ ### Dispatching the set
74
+
75
+ The members of one set dispatch together by design, since they share edge files and the integrator resolves those lines once. A shared file between members is never a hold.
76
+
77
+ - Hold the integrator on a dependency until `plan-answers` reads it launchable
78
+ - Re-read it after a fetch on each member's done message, which carries the branch and head commit in place of a pull request
79
+ - Never read a member with no pull request as stalled, since a member opens none
80
+
81
+ ### What a member writes
82
+
83
+ A member builds, verifies, pushes its branch, and announces it done. A pushed branch is the done signal, so a member pushes once, at the end.
84
+
85
+ A member may edit the shared lists and regenerate the indexes on its own branch, since the pre-push check fails a member that deletes a path a gated list still names. The integrator resolves those lines once at merge.
86
+
87
+ ### What the integrator writes
88
+
89
+ The integrator merges each member branch with `git merge --no-ff` in the order its bullet lists, resolving conflicts by the rules a rebase follows. It then writes its own files, regenerates the indexes once, and opens the pull request.
90
+
91
+ - Say in the pull request body that each member is one merge commit, so a reviewer reads `git log --first-parent` one member at a time
92
+ - Fix a semantic conflict a merge surfaces on the integrator's branch, since a member has no pull request to fix it on
93
+ - Delete each member branch on origin with `git push origin --delete <branch>` once the pull request merges
@@ -105,6 +105,8 @@ A constraint measured against work in flight expires when that work merges, as a
105
105
  - Re-test a stamped constraint before honoring it with `git fetch origin main --quiet && git log <commit>..origin/main --oneline -- <the paths the constraint names>`. Any commit it reports means the track landed and the constraint is dead. Fetch and scope the log to the paths in the same command, since a stale ref reads a dead constraint as live and a squashed subject names no track.
106
106
  - Read an unstamped constraint as unverified rather than as live, and confirm it against the open pull request list for the named paths.
107
107
 
108
+ A batch in a fan-in set declares its side in a constraint bullet, ``Fan-in member of `feature-<integrator>` `` on a member and ``Fans in `feature-<a>`, `feature-<b>` `` on the integrator. `batches.md` holds when to fan in, and `canon records validate plans` reports `fan-in-unmatched` for a set whose two sides disagree.
109
+
108
110
  A constraint naming a `.canon/ready/` folder is a third shape beside the hold and the shared path. It makes that folder's files the verbatim source for the paths this plan's `**Files to touch:**` lists, per `ready.md`, so the executing session copies those paths rather than authoring them.
109
111
 
110
112
  ### Verification
@@ -45,4 +45,4 @@ The batching fails a fifth way. A folder holding thirty unread items put as thir
45
45
  - Promoting an answered item onto the board, which is `task-board` and runs after the answers land
46
46
  - The item format, the answer contract, and retrieval, which the `plan-intake` skill's `references/intake.md` owns and this skill cites
47
47
  - The comparable answer slots in groundwork and feature plans, which carry their own contracts and are a separate measurement
48
- - Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so answering is the operator's call rather than a description match.
48
+ - Deciding when to fire. The description states `Do NOT auto-trigger`, so answering is the operator's call rather than a description match.
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: plan-intake-answer
3
- description: Walks an intake folder's unread items and puts them as batched questions in chat, writing each selection back onto the item it answers. Use when asked to "answer the intake", "work through the open items", "answer my intake questions", "go through the dump", or "let me answer these from here". Do NOT use to file a dump or write new items, which is `plan-intake`.
4
- disable-model-invocation: true
3
+ description: Walks an intake folder's unread items and puts them as batched questions in chat, writing each selection back onto the item it answers. Use when asked to "answer the intake", "work through the open items", "answer my intake questions", "go through the dump", or "let me answer these from here". Do NOT use to file a dump or write new items, which is `plan-intake`. Do NOT auto-trigger.
5
4
  metadata:
6
5
  family: decide
7
6
  ---
@@ -21,6 +21,13 @@ Pop only when the stash ran. On a clean tree, which is what a run whose findings
21
21
 
22
22
  Never merge `main` into the branch. The repository squash-merges, so a merge commit here reads as noise on the pull request.
23
23
 
24
+ ### A branch carrying merges
25
+
26
+ A branch carrying merge commits of its own, such as a fan-in integrator's, rebases with `git rebase --rebase-merges origin/main` instead. A plain rebase drops the merges and replays every member commit in one line, which loses the one merge commit per member a reviewer reads.
27
+
28
+ - `--rebase-merges` re-runs each merge without the resolution it recorded, so resolve each again by the rules below
29
+ - `git config rerere.enabled true`, set before the first merge, replays a recorded resolution in place of resolving it again
30
+
24
31
  ## Resolving a conflict
25
32
 
26
33
  These rules apply to a hunk from the rebase and a hunk from the stash pop alike:
@@ -148,6 +148,8 @@ A console error or an overflow is a finding. A probe result is a reading to conf
148
148
 
149
149
  Read `${CLAUDE_SKILL_DIR}/references/post.md` for the heading rule, the body shape, the marker every body ends on, and the post command. Each box lands in the section its verdict names, and the `link` its Step 5 push returned goes as given into that box's own entry.
150
150
 
151
+ That file sends a pass closing under a prior `## UI review closed` at an older head on to `${CLAUDE_SKILL_DIR}/references/close-out.md`, which rewrites the standing close in place.
152
+
151
153
  ## Step 8: tick the passed boxes
152
154
 
153
155
  Read `${CLAUDE_SKILL_DIR}/references/tick.md` once the verdict has posted, for how every box that passed is ticked through `canon pr tick` at the head this pass drove, and what a refusal reports.
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Rewrite a repeated UI close in place
3
+ description: The branch of review-ui Step 7 taken when the prior verdict closed the review and this pass owes nothing, which replaces the standing close with a PUT rather than posting a second one
4
+ ---
5
+
6
+ # A UI close that repeats the standing one
7
+
8
+ Part of Step 7 of `review-ui`, reached only on the branch `post.md` names. The body shape, the scan, and the marker still come from `post.md`.
9
+
10
+ When the newest UI verdict from Step 1 reads `## UI review closed` at an older head and this pass closes too, the thread already holds this verdict. Replace the standing close rather than posting a second one beside it.
11
+
12
+ Only a closed prior is replaced. A prior `## UI review` stays in the thread when this pass closes, so the close posts normally. A pass that owes anything posts under `## UI review` normally too, whatever the prior read, since a rewrite there would hide a failed box behind a heading that reads closed.
13
+
14
+ Resolve the standing close's numeric id off the REST listing, keeping the newest match across every page:
15
+
16
+ ```bash
17
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## UI review closed") | .id' | tail -n 1
18
+ ```
19
+
20
+ ## Write the replacement
21
+
22
+ Write the full body for this pass to the file `post.md` names, with the heading, the count line, and the Passed table regenerated from what this pass drove. Patching only the marker would leave a count line and an address naming the old head.
23
+
24
+ Add one line under the count line, reading the prior head off the standing verdict's marker and the commit count off the compare:
25
+
26
+ ```bash
27
+ gh api repos/{owner}/{repo}/compare/<prior-head>...<head> --jq .total_commits
28
+ ```
29
+
30
+ ```plaintext
31
+ Re-checked at `<short-sha>`, <N> commits since the prior pass.
32
+ ```
33
+
34
+ Omit the line when the standing verdict carried no marker, since there is no prior head to count from. Run the scan `post.md` states against the file. Then record the instant just before the call, in UTC, as `<posted-at>`, since Step 9 keeps only the review-event runs created after it and the `PUT` fires one on `edited`. Replace the comment:
35
+
36
+ ```bash
37
+ date -u +%Y-%m-%dT%H:%M:%SZ
38
+ gh api -X PUT repos/{owner}/{repo}/pulls/<number>/reviews/<review-id> -F body=@<body-file>
39
+ ```
40
+
41
+ ## The marker
42
+
43
+ The body ends on `<!-- review-ui: head=<head> -->` at the full head this pass drove, with no line below it. `PUT` leaves the review's `commit.oid` pinned at the head the close was first posted against, and the marker is what `resolveUiScope`, the lift gate, and the orchestrator poll read in its place. A body missing it reads as the pinned stamp, so the lift refuses `ui-behind` and the poll reports the close as behind.
44
+
45
+ `PUT` keeps the comment's timestamp and its place in the thread, so the poll's age test reads a rewritten close as old, the same reading the code review accepts. A submitted review cannot be deleted, which is why this rewrites the standing close rather than replacing it with a fresh one.
46
+
47
+ The frames the standing verdict linked sit under their own pass stamp on the frames branch. The new body links this pass's frames, and the old images stay reachable by path.
@@ -104,12 +104,14 @@ The count line and the sections beneath it hold the same totals, and Step 10's p
104
104
 
105
105
  ## The marker
106
106
 
107
- End every body on `<!-- review-ui: head=<head> -->`, carrying the full sha Step 1 resolved. The orchestrator's poll reads it off the last non-empty line to decide whether the verdict covers the current head, and the submission stamp cannot answer that, since GitHub stamps it with whatever the head is at the moment of posting.
107
+ End every body on `<!-- review-ui: head=<head> -->`, carrying the full sha Step 1 resolved. A body that replaces a standing close by `PUT` carries it too, at the head this pass drove. The orchestrator's poll reads it off the last non-empty line to decide whether the verdict covers the current head, and the submission stamp cannot answer that, since GitHub stamps it with whatever the head is at the moment of posting.
108
108
 
109
109
  ## Posting
110
110
 
111
111
  Run `canon labels scan --body-file .canon/tmp/pr/review-ui/body-<number>-<short-sha>.md` against the body first, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`, since nothing gates a file under `.canon/tmp/` before the review-event run does. Fix the body and scan again on a finding. Then post it as a review rather than an issue comment, since the poll reads the UI family off reviews:
112
112
 
113
+ Do not run the post below when the newest verdict from Step 1 reads `## UI review closed` at an older head and this pass closes too. That pass replaces the standing close, so read `${CLAUDE_SKILL_DIR}/references/close-out.md` instead of posting. Posting first leaves two closes, both naming a head.
114
+
113
115
  Record the instant just before the post, in UTC, as `<posted-at>`. Step 9 keeps only review-event runs created after it, since the code review posts its own review on the same head and fires a run of the same gate.
114
116
 
115
117
  ```bash
@@ -45,6 +45,7 @@ Run `canon tasks plan-answers <plan> --json` and read `launchable` off the recor
45
45
 
46
46
  - The command refuses for any other reason, or the record carries no `launchable` key: treat the row as unverified rather than clear, name what could not be read, and fall back to the human. A gate that reads nothing and proceeds is the gate not running.
47
47
  - The same read also refuses a plan still staging its batches in one file with `**Batch N**` sub-headings. That reports as an entry in `open` labeled `Batch staging`, so a staged plan reads as `launchable: false` the same way an unanswered operator call does, and the row waits on a split rather than on the operator.
48
+ - A `Fan-in` entry holds a fan-in set, per `batches.md` in `plan-feature`
48
49
  - An entry labeled `Stacked sibling` means the plan stacks on a live sibling it mostly shares files with, so the row waits on a fold into that sibling or on a `Judged apart from` constraint line, and not on the operator.
49
50
 
50
51
  Branch on `launchable` rather than on the exit code, which a shell function wrapping `canon` can flatten to zero and so read a held row as a clear one.
@@ -28,6 +28,7 @@ The draft mark fails from both ends. On 2026-08-31 four sessions re-drafted a pu
28
28
  - State the shared board as read-only for a building session and name what it does instead when a row is owed, since a task file with no row is a dropped task and placing the row is the controlling session's call
29
29
  - Owe an announcement the moment the pull request opens, carrying the number, the branch, and the task, since that transition is the one interval only the worker can observe
30
30
  - Carry a row the build turned up in the pull request announcement, and send it alone only when the session ends before a pull request opens, since the worker files no task file of its own and the channel is the row's only route
31
+ - Carry a fan-in member's branch and head commit in its done message in place of a pull request number, and end its role at the push, since a member opens no pull request and its integrator fixes what the merge surfaces
31
32
  - 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
32
33
  - Defer the addressee resolution to `session-relay` and keep only the operator ask and the last-rung inference, which discriminates here because a worker holds a feature branch and a controller does not, and which falls back to the roster and says so, since an operator's own launch names nobody
33
34
  - Keep refusing a dispatcher a first-class move carrying its evidence, since the halts measured so far were correct and the cost of each fell where it belonged