@erclx/canon 4.119.0 → 4.121.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 (112) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/SKILL.md +29 -115
  4. package/claude/skills/auto-ship/references/draft-mark.md +30 -0
  5. package/claude/skills/auto-ship/references/failure-recovery.md +27 -0
  6. package/claude/skills/auto-ship/references/tier-failures.md +26 -0
  7. package/claude/skills/auto-ship/references/verb-absent.md +37 -0
  8. package/claude/skills/auto-ship/references/worktree-name.md +22 -0
  9. package/claude/skills/bash-script/SKILL.md +1 -1
  10. package/claude/skills/bash-script/references/patterns.md +0 -99
  11. package/claude/skills/bash-script/references/prompts.md +100 -0
  12. package/claude/skills/create-skill/SKILL.md +3 -2
  13. package/claude/skills/create-snippet/SKILL.md +1 -1
  14. package/claude/skills/create-standard/SKILL.md +1 -1
  15. package/claude/skills/design-extract/SKILL.md +1 -1
  16. package/claude/skills/design-taste/REQUIREMENT.md +2 -2
  17. package/claude/skills/docs-sync/SKILL.md +1 -1
  18. package/claude/skills/draft-context/SKILL.md +1 -1
  19. package/claude/skills/draft-docs/SKILL.md +1 -1
  20. package/claude/skills/draft-figure/SKILL.md +1 -1
  21. package/claude/skills/draft-readme/SKILL.md +1 -1
  22. package/claude/skills/draft-wiki/SKILL.md +1 -1
  23. package/claude/skills/draft-wireframes/SKILL.md +1 -1
  24. package/claude/skills/git-issue/SKILL.md +1 -1
  25. package/claude/skills/git-issue/references/issue.md +1 -1
  26. package/claude/skills/git-pr/REQUIREMENT.md +1 -0
  27. package/claude/skills/git-pr/SKILL.md +35 -5
  28. package/claude/skills/git-stage/SKILL.md +1 -1
  29. package/claude/skills/memory-capture/SKILL.md +1 -1
  30. package/claude/skills/restate-plainly/REQUIREMENT.md +1 -1
  31. package/claude/skills/review-address/SKILL.md +4 -3
  32. package/claude/skills/review-branch/REQUIREMENT.md +1 -0
  33. package/claude/skills/review-branch/SKILL.md +5 -6
  34. package/claude/skills/review-craft/REQUIREMENT.md +43 -0
  35. package/claude/skills/review-craft/SKILL.md +50 -0
  36. package/claude/skills/review-craft/references/security.md +21 -0
  37. package/claude/skills/review-craft/references/ui.md +72 -0
  38. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  39. package/claude/skills/review-pr/SKILL.md +14 -243
  40. package/claude/skills/review-pr/references/close-out.md +30 -0
  41. package/claude/skills/review-pr/references/post.md +145 -0
  42. package/claude/skills/review-pr/references/scope.md +82 -0
  43. package/claude/skills/role-orchestrator/REQUIREMENT.md +9 -16
  44. package/claude/skills/role-orchestrator/SKILL.md +9 -153
  45. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -193
  46. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +22 -0
  47. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +85 -0
  48. package/claude/skills/role-orchestrator/references/orchestrator-output.md +61 -0
  49. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  50. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  51. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +56 -0
  52. package/claude/skills/role-orchestrator/references/orchestrator-review-fallback.md +18 -0
  53. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +1 -1
  54. package/claude/skills/role-planner/REQUIREMENT.md +2 -2
  55. package/claude/skills/standards-audit/SKILL.md +1 -1
  56. package/claude/skills/write-human/REQUIREMENT.md +7 -4
  57. package/claude/skills/write-human/SKILL.md +4 -2
  58. package/claude/skills/write-human/references/machine-tells.md +1 -1
  59. package/claude/skills/write-human/references/source-material.md +5 -2
  60. package/docs/agents/audits.md +1 -1
  61. package/docs/agents/commands.md +3 -2
  62. package/docs/agents/counts.md +1 -1
  63. package/docs/agents/index.md +8 -3
  64. package/docs/agents/markdown-audit.md +13 -19
  65. package/docs/agents/pr-evidence.md +19 -3
  66. package/docs/agents/pr-local.md +105 -0
  67. package/docs/agents/pr-preview.md +2 -2
  68. package/docs/agents/records-push.md +61 -0
  69. package/docs/agents/records-validate.md +73 -0
  70. package/docs/agents/records.md +3 -119
  71. package/docs/agents/sessions-transfer.md +91 -0
  72. package/docs/agents/sessions.md +3 -82
  73. package/docs/agents/skills-audit.md +2 -2
  74. package/docs/agents/teach-authoring.md +112 -0
  75. package/docs/agents/teach.md +3 -103
  76. package/docs/target-projects.md +2 -2
  77. package/docs/workflow/ai-workflow.md +2 -1
  78. package/docs/workflow/visual-design-workflow.md +1 -1
  79. package/governance/rules/claude/500-prose.md +1 -1
  80. package/governance/rules/claude/501-markdown.md +3 -3
  81. package/governance/rules/claude/570-skill.md +2 -1
  82. package/package.json +1 -1
  83. package/scripts/core/check-ignore-parity.sh +1 -1
  84. package/scripts/lib/sandbox-git.sh +1 -1
  85. package/src/autoship/paths.ts +3 -2
  86. package/src/claude/cases/workflow.ts +5 -0
  87. package/src/commands/audits.ts +2 -2
  88. package/src/commands/markdown.ts +7 -9
  89. package/src/commands/pr.ts +331 -2
  90. package/src/context/classify/extract.ts +2 -2
  91. package/src/counts/scan.ts +1 -1
  92. package/src/design/tokens.ts +1 -1
  93. package/src/gate/measures.ts +4 -4
  94. package/src/gate/stages.ts +2 -3
  95. package/src/markdown/bans.ts +11 -68
  96. package/src/markdown/gate.ts +1 -1
  97. package/src/markdown/scan.ts +7 -88
  98. package/src/pr/evidence.ts +42 -2
  99. package/src/pr/local.ts +125 -0
  100. package/standards/docs.md +1 -1
  101. package/standards/index.md +2 -1
  102. package/standards/markdown.md +5 -7
  103. package/standards/pr.md +1 -1
  104. package/standards/publish.md +1 -1
  105. package/standards/readme.md +2 -2
  106. package/standards/skill-requirement.md +72 -0
  107. package/standards/skill.md +34 -152
  108. package/tooling/astro/reference.md +1 -1
  109. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +32 -0
  110. package/tooling/base/configs/scripts/verify.sh +2 -2
  111. package/tooling/base/seeds/canon/context/development.md +1 -1
  112. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +5 -6
package/README.md CHANGED
@@ -162,8 +162,8 @@ Each domain carries an entry written for someone maintaining the toolkit rather
162
162
  - [Governance rules](canon/context/governance/index.md)
163
163
  - [Standards](canon/context/standards/index.md)
164
164
  - [Snippets](canon/context/snippets.md)
165
- - [Tooling stacks](canon/context/tooling.md)
166
- - [Design system](canon/context/design.md)
165
+ - [Tooling stacks](canon/context/tooling/index.md)
166
+ - [Design system](canon/context/design/index.md)
167
167
  - [Slides](canon/context/slides.md)
168
168
  - [Transcripts](canon/context/transcripts.md)
169
169
  - [Sandbox](canon/context/sandbox/index.md)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.119.0",
4
+ "version": "4.121.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -23,17 +23,15 @@ Step 6 classifies the changed-file list to decide whether review runs. Resolve t
23
23
  git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null
24
24
  ```
25
25
 
26
- Prefer `origin/main` over local `main`. A local `main` trailing the remote pulls other people's merged commits into the list, so the classifier decides against files this branch never touched.
26
+ Prefer `origin/main` over local `main`, since a local `main` trailing the remote pulls other people's merged commits into the list.
27
27
 
28
28
  The baseline is unusable when no merge base resolves against either ref. Stop: `❌ No diff baseline against main. Fetch origin, then re-run autoship.`
29
29
 
30
- The base equalling HEAD stays usable here, unlike in the four read-only siblings carrying this section. Step 6 runs before anything is committed, since `git-stage` commits at Step 8, so the base equals HEAD on every ordinary run. The classifier diffs the base against the working tree rather than against HEAD, which keeps the uncommitted work in the set at correct scope. Do not port the sibling `base == HEAD` stop into this skill.
30
+ The base equalling HEAD stays usable here. Step 6 runs before `git-stage` commits at Step 8, so the base equals HEAD on every ordinary run, and the classifier diffs the base against the working tree. Do not port the read-only siblings' `base == HEAD` stop into this skill.
31
31
 
32
32
  ## Step 0: take the role, then enter a worktree
33
33
 
34
- Invoke `canon:role-worker` first, whatever the worktree state. This session is about to build one branch under one plan, and that skill states the boundaries, the lifetime, and the channel obligations the role carries. A dispatched worker reaches the role here and nowhere else, since the launch names this chain rather than the role, and a hand-launched one reaches it on the same path.
35
-
36
- Report it rather than proceeding silently when `canon:role-worker` does not resolve. It ships with the plugin, so a session running this chain from a project holding the CLI alone builds with no role asserted.
34
+ Invoke `canon:role-worker` first, whatever the worktree state. That skill states the boundaries, the lifetime, and the channel obligations of a session building one branch under one plan, and a dispatched worker reaches the role here and nowhere else. Report it rather than proceeding silently when `canon:role-worker` does not resolve.
37
35
 
38
36
  If `git rev-parse --git-dir` equals `git rev-parse --git-common-dir`, the session is in the main worktree. Invoke `canon:session-worktree` before continuing, carrying the argument the subsection below derives. The wrapper handles branch alignment. Do not call `EnterWorktree` directly.
39
37
 
@@ -43,45 +41,23 @@ If the two commands differ, the session is already in a linked worktree. Continu
43
41
 
44
42
  ### Name the worktree from the plan rather than leaving it to be derived
45
43
 
46
- This step runs ahead of Step 1, so what it holds is the raw invocation argument rather than a resolved plan. When that argument is a plan path or a bare slug, run the verb on it and hand the result to `canon:session-worktree` as its tier 0 argument:
47
-
48
- ```bash
49
- canon tasks plan-branch <argument> --json
50
- ```
51
-
52
- - `conforms: true`: pass the record's `branch`, which is already `<type>/<slug>`, and invoke nothing else to derive a name.
53
- - `conforms: false`: the plan's own filename breaks a cap in `${CLAUDE_SKILL_DIR}/../../standards/branch.md`. Pass `branch` anyway and say the cap it broke, since the alternative is a name this session shortened by hand, which is a second derivation and the thing this call exists to prevent. `git-branch` decides the rename at ship.
54
- - Anything else, including a refusal, a record carrying no `branch` key, and an installed binary carrying no `plan-branch` subcommand: invoke `canon:session-worktree` bare and let its ladder derive the name. Say the verb did not answer, so a reader can tell a derived name from a fallback one.
55
-
56
- The dispatch runbook runs the same verb on the same plan to pick the branch its collision check clears, so calling it here is what makes the checked branch and the taken branch one string. Deriving a name from `<plan>` by reading it was the alternative, and two readings of one plan part on the slug and on the type alike.
57
-
58
- A caller that supplied a task path, or supplied nothing at all, has no plan to hand the verb here, since resolving either is Step 1's work and Step 1 has not run. Invoke `canon:session-worktree` bare in both cases. That is the ladder unchanged rather than a regression, and it leaves the hole open: a dispatched worker reaching this step through a task path derives its name from a tier rather than from the plan the dispatcher checked.
44
+ When the invocation argument is a plan path or a bare slug, name the worktree from `canon tasks plan-branch <argument> --json` rather than from a reading of the plan. Read `${CLAUDE_SKILL_DIR}/references/worktree-name.md` before invoking `canon:session-worktree` for how each record shape reaches its tier 0 argument. A task path or no argument at all invokes it bare.
59
45
 
60
46
  ## Step 1: read the plan
61
47
 
62
48
  Resolve `<plan>` in this order, stopping at the first match:
63
49
 
64
- 1. **Caller-supplied task.** The invocation carried a path under `.canon/tasks/`. If it does not resolve to a file, stop: `❌ No task at <path>. Path was supplied, not derived, so check it and re-run.` Read that task's first `Plan:` line and take what it names as `<plan>`, per `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`.
50
+ 1. **Caller-supplied task.** The invocation carried a path under `.canon/tasks/`. If it does not resolve to a file, stop: `❌ No task at <path>. Path was supplied, not derived, so check it and re-run.` Read that task's first `Plan:` line and take what it names as `<plan>`, per `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`, reading `${CLAUDE_SKILL_DIR}/references/tier-failures.md` first for how the pointer resolves and the three stops it can take.
65
51
  2. **Caller-supplied plan.** The invocation carried something else. Accept it as a plan path or a bare slug, in the same position `session-worktree` tier 0 accepts its name. A bare slug resolves to `.canon/plans/feature-<slug>.md`, and a path is taken as given from the main worktree root. If it does not resolve to a file, stop: `❌ No plan at <path>. Path was supplied, not derived, so check it and re-run.`
66
52
  3. **Derived.** `.canon/plans/feature-<slug>.md`, from the `<slug>` the Guards derived. If it does not exist, stop: `❌ No approved plan at .canon/plans/feature-<slug>.md, where <slug> was derived from the current branch, <branch>. The invocation carried no argument, so pass a plan or a task path, or run /plan-feature first.`
67
53
 
68
- Only a path reaches tier 1, and a bare slug is read as a plan's throughout. The two would collide on any similar name, and a caller who means the task holds its path already, having read it off the board. One plan per task is what makes the tier 1 read unambiguous, so it takes the first `Plan:` line and never scans for a second.
69
-
70
- Read the target out of the link's parentheses, and take the rest of the line when the line carries no link, since an older task writes the target as a plain path with nothing around it. Resolve a relative target against the directory holding the task file rather than against `.canon/tasks/`, and take a project-root target from the root. The archived task is what makes that base matter, since the standard points its line at `../../plans/archive/feature-<slug>.md` once the task sits a folder deeper, and reading that from `.canon/tasks/` lands on a repository-root `plans/archive/` that never exists.
54
+ Only a path reaches tier 1, and a bare slug is read as a plan's throughout, since the two would collide on any similar name.
71
55
 
72
56
  ### When a tier fails
73
57
 
74
- Tier 1 stops on three failures, and each names a different repair:
75
-
76
- - No `Plan:` line at all. Stop: `❌ <path> carries no Plan: line, so nothing there names a plan to run. Write the plan and point the task at it, or pass the plan path directly.` A row still awaiting a plan is the ordinary case, so the message names the missing pointer rather than the missing plan sections a reader would then go hunting for.
77
- - The pointer resolves into a plans archive. Stop: `❌ <path> points at an archived plan, which describes work that already shipped. Reopen the task against a live plan, or pass that plan directly.` Test the resolved path rather than the task's outcomes or its `Pull request:` line, since a stale board gets its ticks wrong and the standard fixes where a shipped pointer lands.
78
- - The pointer resolves to no file. Stop: `❌ <path> points at <target>, which does not exist. The citation is stale, so repoint the task or pass the plan path directly.`
79
-
80
- An archive is `.canon/plans/archive/` and also the two older spellings `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` leaves in place for a project that archived plans before the folder nested, written from a task as `../plans-archive/` and `../.tmp/plans-archive/`. Test all three, since a shipped pointer in a project nobody migrated lands on the older two. Run the archive test ahead of the existence test, so a pointer into an archive that no longer holds the file still refuses as shipped work rather than as a stale citation.
81
-
82
- Each tier fails for a different reason and says so. A supplied path resolving to nothing is a typo, a derived path resolving to nothing is a plan nobody wrote, and a task pointer resolving to nothing is a stale citation the board should have caught.
58
+ Tier 1 stops when the task carries no `Plan:` line, when its pointer resolves into a plans archive, or when its pointer resolves to no file. `${CLAUDE_SKILL_DIR}/references/tier-failures.md` carries the stop each one takes and the order the archive and existence tests run in.
83
59
 
84
- Test the shape of whatever `<plan>` resolved to before reading it as one. A file resolving under any tier can still be the wrong document, and tier 1 resolves through a pointer rather than from the caller, so the test runs after all three rather than guarding a supplied path alone.
60
+ Test the shape of whatever `<plan>` resolved to before reading it as one, after all three tiers, since a file resolving under any tier can still be the wrong document.
85
61
 
86
62
  Check for a `**Files to touch:**` or `## Files to touch` marker per `${CLAUDE_SKILL_DIR}/../../standards/plan.md`, the one section every plan carries structurally and a task never does, where `## Outcomes` and `## Findings` are the task's own. If neither form is present, stop: `❌ <path> carries no plan sections. A plan lives at .canon/plans/feature-<slug>.md, and a task reaches one through its Plan: line only from .canon/tasks/. Point autoship at either and re-run.`
87
63
 
@@ -105,17 +81,17 @@ Do not loop. Do not bypass hooks.
105
81
 
106
82
  ## Step 4: test order
107
83
 
108
- Run `canon gov test-order --json` from the worktree this chain is building in, which is the one tree holding the branch's own commits. The Guards send a `.canon/plans/` and a `.canon/review/` read to the main worktree root and this is not one of those: that checkout sits on the trunk on an ordinary run, so the range closes on itself there and every finding the branch carries reads as clean.
84
+ Run `canon gov test-order --json` from the worktree this chain is building in, never from the main worktree root, whose trunk checkout closes the range on itself and reads every finding as clean.
109
85
 
110
- The verb reads git history rather than the working tree, so a branch with nothing committed yet reads as clean, and what it catches is a violation that already reached this branch's history in an earlier session or an earlier round of this chain. It reports and never gates, since nothing wires its exit into a push, so branch on the record's `kind` and its `findings` array rather than on the exit, which reads 2 on a finding and 1 on a refusal and which a shell function wrapping `canon` can flatten to zero either way.
86
+ The verb reads git history rather than the working tree, so it catches a violation that already reached this branch's history in an earlier session or round. It reports and never gates, so branch on the record's `kind` and its `findings` array rather than on the exit, which a shell function wrapping `canon` can flatten to zero.
111
87
 
112
88
  - `kind: 'measured'`, `findings` empty. Nothing on the branch reached history ahead of its test. Continue to Step 5.
113
- - `kind: 'measured'`, `findings` non-empty. Report each pair's `subject` and `reason` to the user and continue to Step 5. Do not stop the chain and do not attempt a fix: the commit already reached history, and rewriting it here rewrites what an earlier run shipped rather than what this run is building.
114
- - `kind: 'unreadable'`. Report the `message` and continue to Step 5. A shallow clone or a repository with no trunk is an ordinary state this check cannot read, not a reason to stop shipping.
89
+ - `kind: 'measured'`, `findings` non-empty. Report each pair's `subject` and `reason` to the user and continue to Step 5. Do not stop the chain and do not attempt a fix, since rewriting it here rewrites what an earlier run shipped.
90
+ - `kind: 'unreadable'`. Report the `message` and continue to Step 5, since a shallow clone or a repository with no trunk is no reason to stop shipping.
115
91
 
116
92
  ### When the verb is absent
117
93
 
118
- The verb ships with the CLI and this body ships with the plugin, matching Step 6's own fallback for the classify verb. Report that the check did not run rather than reading a missing subcommand as clean, and continue to Step 5.
94
+ Never read a missing subcommand as clean. Read `${CLAUDE_SKILL_DIR}/references/verb-absent.md` on meeting one.
119
95
 
120
96
  ## Step 5: UI checklist (conditional)
121
97
 
@@ -123,7 +99,7 @@ If the diff touches UI files (JSX, TSX, Vue, Svelte, HTML, or CSS under `src/`),
123
99
 
124
100
  If `ui-checklist` produces a checklist, stop: `❌ UI requires visual verification. Checklist at .canon/tmp/handoff/ui-checklist/<slug>.md, which reaches the pull request once /git-ship runs. Verify manually, then run /git-ship.`
125
101
 
126
- The stop is unchanged by that skill's shrink. The checklist is now the step's whole output rather than the visual remainder beside the tests it wrote, so the stop is the only thing that makes the operator look before `git-ship`.
102
+ The stop is the only thing that makes the operator look before `git-ship`.
127
103
 
128
104
  If there is nothing to verify visually and nothing shipping untested, continue.
129
105
 
@@ -135,84 +111,41 @@ Classify the diff first. Take the union of `git diff --name-only <base>` and `gi
135
111
  canon autoship classify --json <path>...
136
112
  ```
137
113
 
138
- The verb reads names only and touches git not at all, so the set stays the one this step already computed and no second baseline resolves to disagree with the first. Branch on the record's `decision` rather than on the exit code, which a shell function wrapping `canon` can flatten to zero.
114
+ The verb reads names only and touches git not at all. Branch on the record's `decision` rather than on the exit code, which a shell function wrapping `canon` can flatten to zero.
139
115
 
140
116
  - `skip`. Every path reads as prose and none states agent behavior. Skip review entirely and continue to Step 8.
141
117
  - `review`. Invoke `canon:review-branch`. The record names the `file` that decided it and the `test` it failed, `extension` for a path that is not prose and `behavior-path` for prose that states what an agent does.
142
118
  - `refused`, carrying reason `no-changes`. The changed set was empty, so take the stop below.
143
119
 
144
- Say in the run which of the two decided, the verb or the written fallback, since a reader otherwise cannot tell a classification from a judgment.
120
+ Say in the run whether the verb or the written fallback decided.
145
121
 
146
- An empty list stops the chain: `❌ No changed files to classify. Re-run when the plan has yet to produce its output. When the output is gitignored by design, autoship cannot ship it, so take the work out of the chain.` An empty list satisfies the prose-only test vacuously, so reading it as prose-only routes the branch past review instead of through it.
147
-
148
- The two causes want different responses. A plan that has yet to produce its output is a re-run once it has. A plan whose output is gitignored by design, such as a read pass writing to `.canon/tmp/`, is work the chain cannot carry at all, since `git-stage` finds nothing to commit six steps later. Never advise removing the output from `.gitignore`, which trades a stopped run for scratch committed into the repository.
122
+ An empty list stops the chain rather than reading as prose-only, which it satisfies vacuously: `❌ No changed files to classify. Re-run when the plan has yet to produce its output. When the output is gitignored by design, autoship cannot ship it, so take the work out of the chain.` Never advise removing the output from `.gitignore`, which trades a stopped run for scratch committed into the repository.
149
123
 
150
124
  ### When the verb is absent
151
125
 
152
- The verb ships with the CLI and this body ships with the plugin, so a target holding an older binary meets a missing subcommand. Apply the written test by hand there, and say the fallback decided it.
153
-
154
- Never read an absent subcommand as a skip. Failing open is the exact defect the verb closes, and a shell that answers `command not found` reaching a body that skips on anything other than a `skip` record would ship every branch unreviewed.
155
-
156
- The skip needs both tests to pass: every changed file matches `*.md` or `*.txt`, and no changed file sits under a behavior path. On a pass, skip review entirely and continue to Step 8. Otherwise invoke `canon:review-branch`.
157
-
158
- Behavior paths carry two spellings, the one a surface authors at and the one it reaches a session at, so the rule reads the same in a toolkit and in a project that consumed one:
159
-
160
- - `claude/skills/` and `.claude/skills/`
161
- - `governance/rules/` and `.claude/rules/`
162
- - `standards/`, which is the authoring root and reaches a reader by resolution rather than by an install, so it carries no `.claude/` spelling
163
- - `snippets/`, which reaches a session through the `claude/snippets` symlink rather than an install, so it carries no `.claude/` spelling either
164
- - `internal/` and `tooling/`, which hold the stack references and the seed documents a target is handed
165
- - `CLAUDE.md` at the repository root, named as a file because a path prefix reaches nothing that sits in no folder
166
-
167
- Markdown under one of them states what an agent does, so a change there is a behavior change wearing a prose extension. Everything outside them is informational, which keeps `docs/`, `README.md`, and `CHANGELOG.md` skipping without naming them. One behavior file sends the whole branch to review, since documentation shipped beside a behavior change does not cancel it.
168
-
169
- Informational prose is already gated by `docs-sync`, `standards-audit`, and pre-push hooks. Running a code-style review on it burns tokens with no signal.
170
-
171
- The list covers this toolkit's authoring layout and the layout it installs, which is not every layout. A project keeping executable prose where neither spelling reaches adds the path, and until it does every branch touching it skips review silently.
172
-
173
- The verb reads the same set from `src/autoship/paths.ts`, so a path added here belongs there too and a path added there belongs here. Two copies is what the fallback costs, and it stands until a release retires the written half.
126
+ Never read an absent subcommand as a skip, since failing open would ship every branch unreviewed. Read `${CLAUDE_SKILL_DIR}/references/verb-absent.md` on meeting one, which carries the written test and the behavior paths it reads.
174
127
 
175
128
  ## Step 7: evaluate findings
176
129
 
177
- Skip this step when Step 6 skipped review. Otherwise read `.canon/review/branch-<slug>.md` at the main worktree root. Split every finding by origin before parsing the summary line (`X critical, Y should-fix, Z minor`), since the stop exists for a defect the branch inherited rather than for one this run introduced.
130
+ Skip this step when Step 6 skipped review. Otherwise read `.canon/review/branch-<slug>.md` at the main worktree root. Split every finding by origin before parsing the summary line (`X critical, Y should-fix, Z minor`), since the stop exists for a defect the branch inherited.
178
131
 
179
- - **This run caused it, at any severity.** Fix it, re-run the Step 3 verify commands, re-read the fixed file against what the finding claimed, and continue. Do not report it as a stop and do not offer the fix as a choice, which is the same stop wearing a proposal.
132
+ - **This run caused it, at any severity.** Fix it, re-run the Step 3 verify commands, re-read the fixed file against what the finding claimed, and continue. Do not report it as a stop or offer the fix as a choice.
180
133
  - **It predates this run, critical or should-fix.** Stop: `❌ Review found non-minor issues that predate this run. See .canon/review/branch-<slug>.md. Fix and run /git-ship.`
181
134
  - **It predates this run, minor only.** Continue. The minor findings stay in the on-disk review receipt. Fold any a reviewer needs into the PR's `## Technical Context`. Do not add a separate review-notes section to the PR body.
182
135
 
183
- Read origin as causation rather than authorship. Staleness this run induced in a file it never opened is a finding it caused, and the plan's "Files to touch" list scopes what the run builds rather than what it may repair.
136
+ Read origin as causation rather than authorship, so staleness this run induced in a file it never opened is a finding it caused.
184
137
 
185
138
  Bound the repair at one pass, the way Step 3 bounds verify. When that re-read shows the finding still standing, stop: `❌ A self-introduced finding survived one fix pass. See .canon/review/branch-<slug>.md. Fix and run /git-ship.`
186
139
 
187
- This chain owns the receipt's lifetime, which is what makes the Output block's citation resolve on a run that reaches it. `docs-fold` used to delete the current slug's receipt while running under Step 8 below, so the closing line named a file the same run had already removed. That sweep now reaches only reports whose branch is gone, which collects this one a branch later rather than during the run that wrote it. The cost is one `branch-<slug>.md` receipt per live branch left in `.canon/review/`, bounded by the branch count rather than by the lifetime of the checkout.
140
+ This chain owns the receipt's lifetime, which is what makes the Output block's citation resolve on a run that reaches it. The `docs-fold` sweep under Step 8 reaches only reports whose branch is gone, which collects this one a branch later rather than during the run that wrote it.
188
141
 
189
142
  ## Step 8: ship
190
143
 
191
- Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, and the CI watch, along with the reason each step sits where it does. This step used to restate that list and the two drifted apart with nothing comparing them, so read the order there and never here.
192
-
193
- One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch. Run it as one command, where the shell proves the target before the mark and reads the flag back after it:
194
-
195
- ```bash
196
- target=$(gh pr view <number> --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
197
- if [ "$target" != "$(git branch --show-current) OPEN" ]; then
198
- printf '❌ #%s reads "%s", not "%s OPEN". The draft mark was not written.\n' "<number>" "$target" "$(git branch --show-current)" >&2
199
- exit 1
200
- fi
201
- gh pr ready --undo <number>
202
- gh pr view <number> --json isDraft
203
- ```
204
-
205
- Name the number `git-ship`'s pull request step returned in every position rather than leaving any to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
206
-
207
- A refusal is a stop. Report the line the shell printed, then resolve the number from the `number=` and `head=` lines of `git-pr`'s output and re-run the command by hand. The comparison sits in the shell rather than in the session's reading of a printed value, which is the same split `git-pr`'s `assert_own_pr` makes, because the judgment that picked a wrong number is the one that would read the check.
208
-
209
- The number crosses from that step's output into a command this session types, which is the one place a number gets authored rather than derived. A number misread there, or inferred from the newest pull request in view, would draft a stranger's pull request. A release pull request is the worst such target, since the mark holds the release and this role may not lift it again.
210
-
211
- Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
144
+ Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, and the CI watch, along with the reason each step sits where it does. Read the order there and never here.
212
145
 
213
- Placement is why the call sits ahead of the watch rather than after it. Marking afterwards leaves the pull request unmarked for the whole CI run, which is the stretch an unattended worker's branch is least supervised. What the mark buys is a reader learning the pull request has had no review yet. It buys no bound on that stretch: readying a pull request to merge lifts the mark, GitHub requires it before a merge, and the act belongs to the operator or to the controlling session that closed the review, whichever it is, taken directly on the pull request rather than delegated to a worker. `role-worker` states the mirroring refusal.
146
+ One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch, naming the number that step returned rather than one resolved by branch. Read `${CLAUDE_SKILL_DIR}/references/draft-mark.md` on reaching that point for the one command that proves the target, marks it, and reads the flag back. A `false` read stops the chain, and never re-issue the mark on it.
214
147
 
215
- `git-ship` verifies again at its own gate, which repeats this chain's Step 3 on the run where nothing stopped. That cost is deliberate: four of the stop points in the table below hand the run straight back to that body, and a gate the chain skips for being redundant is a gate no resumed run ever meets.
148
+ `git-ship` verifies again at its own gate, which repeats this chain's Step 3 on the run where nothing stopped. That cost is deliberate, since most stop points hand the run straight back to that body, and a gate the chain skips for being redundant is a gate no resumed run ever meets.
216
149
 
217
150
  ## Output
218
151
 
@@ -225,31 +158,12 @@ Respond with up to four lines:
225
158
  <N memories captured in .canon/memory/>
226
159
  ```
227
160
 
228
- `<state>` is whatever the Step 8 read returned, being `draft` or `ready, unsupervised`, rather than the state the undo asked for. Writing the word `draft` there unconditionally is what this line used to do, and it named a state no step had read.
161
+ `<state>` is whatever the Step 8 read returned, being `draft` or `ready, unsupervised`, rather than the state the undo asked for.
229
162
 
230
- Omit the second line if there were no minor findings, and the third if nothing routed. Omit the fourth if `memory-capture` wrote no memory file this session. A run that routes every fact and writes none is the shape to expect, and it reports three lines.
163
+ Omit the second line if there were no minor findings, and the third if nothing routed. Omit the fourth if `memory-capture` wrote no memory file this session.
231
164
 
232
- This block replaces the one `git-ship` closes on rather than following it. The two carry the same two trailing lines and differ on the two above them, since the first names the state the read returned and the second reports the minor findings Step 7 kept, neither of which that body has a counterpart for. Emitting both reports one run twice and buries the state under a `✅ Shipped` that does not name it.
165
+ This block replaces the one `git-ship` closes on rather than following it, since emitting both reports one run twice and buries the state under a `✅ Shipped` that does not name it.
233
166
 
234
167
  ## Failure recovery
235
168
 
236
- Every stop point leaves recoverable state. The user resumes manually from the appropriate step.
237
-
238
- | Stop point | Recovery |
239
- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
240
- | No plan (derived) | Run `/plan-feature` to create one |
241
- | No plan (caller-supplied) | Check the path or slug passed to autoship, then re-run |
242
- | No task (caller-supplied) | Check the task path passed to autoship, then re-run |
243
- | Task carries no `Plan:` line | Write the plan, point the task's `Plan:` line at it, then re-run |
244
- | Task points at an archived plan | The work already shipped. Reopen the task against a live plan, or pass that plan's path directly. |
245
- | Task's `Plan:` pointer resolves to nothing | Repoint the task's `Plan:` line at the plan that exists, then re-run |
246
- | Resolved file carries no plan shape | Point autoship at a plan under `.canon/plans/feature-<slug>.md` or at a task under `.canon/tasks/`, then re-run |
247
- | No diff baseline | Fetch origin so a merge base resolves against `main`, then re-run autoship |
248
- | Empty changed-file list | Re-run once the plan produces tracked output. Ship gitignored output outside the chain, never by tracking it. |
249
- | Branch collision on worktree entry | `session-worktree` Step 5 found `<slug>` already as a local branch. Resolve manually (rename or delete the stale branch), then re-run autoship. |
250
- | Verify fails | Read logs, fix manually, run `/git-ship` |
251
- | UI checklist | Verify visually, run `/git-ship` |
252
- | Inherited review findings | Fix findings, run `/git-ship` |
253
- | Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
254
- | git-ship fails | Inspect hook or remote error, run again |
255
- | Draft target is not this branch's | Nothing was marked. Read the number and `head=` from `git-pr`'s output, confirm they match, then run the mark by hand |
169
+ Every stop point leaves recoverable state. Read `${CLAUDE_SKILL_DIR}/references/failure-recovery.md` on a stop for the step the user resumes from.
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Draft mark
3
+ description: The one command auto-ship Step 8 runs as git-ship's pull request step returns, which proves the target is this branch's open pull request, marks it a draft, and reads the flag back
4
+ ---
5
+
6
+ # Mark the pull request a draft
7
+
8
+ Step 8 of `auto-ship`. The session reads this file as `git-ship`'s pull request step returns, ahead of its CI watch.
9
+
10
+ Run the mark as one command, where the shell proves the target before the mark and reads the flag back after it:
11
+
12
+ ```bash
13
+ target=$(gh pr view <number> --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
14
+ if [ "$target" != "$(git branch --show-current) OPEN" ]; then
15
+ printf '❌ #%s reads "%s", not "%s OPEN". The draft mark was not written.\n' "<number>" "$target" "$(git branch --show-current)" >&2
16
+ exit 1
17
+ fi
18
+ gh pr ready --undo <number>
19
+ gh pr view <number> --json isDraft
20
+ ```
21
+
22
+ Name the number `git-ship`'s pull request step returned in every position rather than leaving any to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
23
+
24
+ A refusal is a stop. Report the line the shell printed, then resolve the number from the `number=` and `head=` lines of `git-pr`'s output and re-run the command by hand. The comparison sits in the shell rather than in the session's reading of a printed value, which is the same split `git-pr`'s `assert_own_pr` makes, because the judgment that picked a wrong number is the one that would read the check.
25
+
26
+ The number crosses from that step's output into a command this session types, which is the one place a number gets authored rather than derived. A number misread there, or inferred from the newest pull request in view, would draft a stranger's pull request. A release pull request is the worst such target, since the mark holds the release and this role may not lift it again.
27
+
28
+ Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
29
+
30
+ Placement is why the call sits ahead of the watch rather than after it. Marking afterwards leaves the pull request unmarked for the whole CI run, which is the stretch an unattended worker's branch is least supervised. What the mark buys is a reader learning the pull request has had no review yet. It buys no bound on that stretch: readying a pull request to merge lifts the mark, GitHub requires it before a merge, and the act belongs to the operator or to the controlling session that closed the review, whichever it is, taken directly on the pull request rather than delegated to a worker. `role-worker` states the mirroring refusal.
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: Failure recovery
3
+ description: The step an operator resumes from after each auto-ship stop point, since every stop leaves the code on the branch, the review on disk, and the plan linked
4
+ ---
5
+
6
+ # Failure recovery
7
+
8
+ Every stop point leaves recoverable state. The user resumes manually from the appropriate step.
9
+
10
+ | Stop point | Recovery |
11
+ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
12
+ | No plan (derived) | Run `/plan-feature` to create one |
13
+ | No plan (caller-supplied) | Check the path or slug passed to autoship, then re-run |
14
+ | No task (caller-supplied) | Check the task path passed to autoship, then re-run |
15
+ | Task carries no `Plan:` line | Write the plan, point the task's `Plan:` line at it, then re-run |
16
+ | Task points at an archived plan | The work already shipped. Reopen the task against a live plan, or pass that plan's path directly. |
17
+ | Task's `Plan:` pointer resolves to nothing | Repoint the task's `Plan:` line at the plan that exists, then re-run |
18
+ | Resolved file carries no plan shape | Point autoship at a plan under `.canon/plans/feature-<slug>.md` or at a task under `.canon/tasks/`, then re-run |
19
+ | No diff baseline | Fetch origin so a merge base resolves against `main`, then re-run autoship |
20
+ | Empty changed-file list | Re-run once the plan produces tracked output. Ship gitignored output outside the chain, never by tracking it. |
21
+ | Branch collision on worktree entry | `session-worktree` Step 5 found `<slug>` already as a local branch. Resolve manually (rename or delete the stale branch), then re-run autoship. |
22
+ | Verify fails | Read logs, fix manually, run `/git-ship` |
23
+ | UI checklist | Verify visually, run `/git-ship` |
24
+ | Inherited review findings | Fix findings, run `/git-ship` |
25
+ | Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
26
+ | git-ship fails | Inspect hook or remote error, run again |
27
+ | Draft target is not this branch's | Nothing was marked. Read the number and `head=` from `git-pr`'s output, confirm they match, then run the mark by hand |
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: Tier 1 task reads
3
+ description: How auto-ship Step 1 resolves a caller-supplied task's Plan line to a plan, and the three stops it takes when that pointer is missing, archived, or resolves to no file
4
+ ---
5
+
6
+ # Reading a task's plan pointer
7
+
8
+ Step 1 of `auto-ship`, tier 1. The session reads this file when the invocation carried a path under `.canon/tasks/`.
9
+
10
+ ## Reading the pointer
11
+
12
+ One plan per task is what makes the tier 1 read unambiguous, so it takes the first `Plan:` line and never scans for a second. A caller who means the task holds its path already, having read it off the board, which is why a bare slug never reaches this tier.
13
+
14
+ Read the target out of the link's parentheses, and take the rest of the line when the line carries no link, since an older task writes the target as a plain path with nothing around it. Resolve a relative target against the directory holding the task file rather than against `.canon/tasks/`, and take a project-root target from the root. The archived task is what makes that base matter, since the standard points its line at `../../plans/archive/feature-<slug>.md` once the task sits a folder deeper, and reading that from `.canon/tasks/` lands on a repository-root `plans/archive/` that never exists.
15
+
16
+ ## When a tier fails
17
+
18
+ Tier 1 stops on three failures, and each names a different repair:
19
+
20
+ - No `Plan:` line at all. Stop: `❌ <path> carries no Plan: line, so nothing there names a plan to run. Write the plan and point the task at it, or pass the plan path directly.` A row still awaiting a plan is the ordinary case, so the message names the missing pointer rather than the missing plan sections a reader would then go hunting for.
21
+ - The pointer resolves into a plans archive. Stop: `❌ <path> points at an archived plan, which describes work that already shipped. Reopen the task against a live plan, or pass that plan directly.` Test the resolved path rather than the task's outcomes or its `Pull request:` line, since a stale board gets its ticks wrong and the standard fixes where a shipped pointer lands.
22
+ - The pointer resolves to no file. Stop: `❌ <path> points at <target>, which does not exist. The citation is stale, so repoint the task or pass the plan path directly.`
23
+
24
+ An archive is `.canon/plans/archive/` and also the two older spellings `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` leaves in place for a project that archived plans before the folder nested, written from a task as `../plans-archive/` and `../.tmp/plans-archive/`. Test all three, since a shipped pointer in a project nobody migrated lands on the older two. Run the archive test ahead of the existence test, so a pointer into an archive that no longer holds the file still refuses as shipped work rather than as a stale citation.
25
+
26
+ Each tier fails for a different reason and says so. A supplied path resolving to nothing is a typo, a derived path resolving to nothing is a plan nobody wrote, and a task pointer resolving to nothing is a stale citation the board should have caught.
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: Verbs absent from an older binary
3
+ description: What auto-ship Steps 4 and 6 do when the installed canon binary carries no test-order or autoship classify subcommand, including the written review test and its behavior paths
4
+ ---
5
+
6
+ # When the verb is absent
7
+
8
+ Steps 4 and 6 of `auto-ship`. The session reads this file when the installed binary answers either step's verb with a missing subcommand, since each verb ships with the CLI and this body ships with the plugin.
9
+
10
+ ## Step 4: test order
11
+
12
+ The verb ships with the CLI and this body ships with the plugin, matching Step 6's own fallback for the classify verb. Report that the check did not run rather than reading a missing subcommand as clean, and continue to Step 5.
13
+
14
+ ## Step 6: review
15
+
16
+ The verb ships with the CLI and this body ships with the plugin, so a target holding an older binary meets a missing subcommand. Apply the written test by hand there, and say the fallback decided it.
17
+
18
+ Never read an absent subcommand as a skip. Failing open is the exact defect the verb closes, and a shell that answers `command not found` reaching a body that skips on anything other than a `skip` record would ship every branch unreviewed.
19
+
20
+ The skip needs both tests to pass: every changed file matches `*.md` or `*.txt`, and no changed file sits under a behavior path. On a pass, skip review entirely and continue to Step 8. Otherwise invoke `canon:review-branch`.
21
+
22
+ Behavior paths carry two spellings, the one a surface authors at and the one it reaches a session at, so the rule reads the same in a toolkit and in a project that consumed one:
23
+
24
+ - `claude/skills/` and `.claude/skills/`
25
+ - `governance/rules/` and `.claude/rules/`
26
+ - `standards/`, which is the authoring root and reaches a reader by resolution rather than by an install, so it carries no `.claude/` spelling
27
+ - `snippets/`, which reaches a session through the `claude/snippets` symlink rather than an install, so it carries no `.claude/` spelling either
28
+ - `internal/` and `tooling/`, which hold the stack references and the seed documents a target is handed
29
+ - `CLAUDE.md` at the repository root, named as a file because a path prefix reaches nothing that sits in no folder
30
+
31
+ Markdown under one of them states what an agent does, so a change there is a behavior change wearing a prose extension. Everything outside them is informational, which keeps `docs/`, `README.md`, and `CHANGELOG.md` skipping without naming them. One behavior file sends the whole branch to review, since documentation shipped beside a behavior change does not cancel it.
32
+
33
+ Informational prose is already gated by `docs-sync`, `standards-audit`, and pre-push hooks. Running a code-style review on it burns tokens with no signal.
34
+
35
+ The list covers this toolkit's authoring layout and the layout it installs, which is not every layout. A project keeping executable prose where neither spelling reaches adds the path, and until it does every branch touching it skips review silently.
36
+
37
+ The verb reads the same set from `src/autoship/paths.ts`, so a path added here belongs there too and a path added there belongs here. Two copies is what the fallback costs, and it stands until a release retires the written half.
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: Worktree name from the plan
3
+ description: How auto-ship Step 0 names the worktree it enters from the plan the caller supplied, through canon tasks plan-branch, so the branch a dispatcher checked and the branch the worker takes are one string
4
+ ---
5
+
6
+ # Name the worktree from the plan
7
+
8
+ Step 0 of `auto-ship`. The session reads this file when it is in the main worktree and about to invoke `canon:session-worktree`.
9
+
10
+ This step runs ahead of Step 1, so what it holds is the raw invocation argument rather than a resolved plan. When that argument is a plan path or a bare slug, run the verb on it and hand the result to `canon:session-worktree` as its tier 0 argument:
11
+
12
+ ```bash
13
+ canon tasks plan-branch <argument> --json
14
+ ```
15
+
16
+ - `conforms: true`: pass the record's `branch`, which is already `<type>/<slug>`, and invoke nothing else to derive a name.
17
+ - `conforms: false`: the plan's own filename breaks a cap in `${CLAUDE_SKILL_DIR}/../../standards/branch.md`. Pass `branch` anyway and say the cap it broke, since the alternative is a name this session shortened by hand, which is a second derivation and the thing this call exists to prevent. `git-branch` decides the rename at ship.
18
+ - Anything else, including a refusal, a record carrying no `branch` key, and an installed binary carrying no `plan-branch` subcommand: invoke `canon:session-worktree` bare and let its ladder derive the name. Say the verb did not answer, so a reader can tell a derived name from a fallback one.
19
+
20
+ The dispatch runbook runs the same verb on the same plan to pick the branch its collision check clears, so calling it here is what makes the checked branch and the taken branch one string.
21
+
22
+ A caller that supplied a task path, or supplied nothing at all, has no plan to hand the verb here, since resolving either is Step 1's work. Invoke `canon:session-worktree` bare in both cases.
@@ -7,7 +7,7 @@ description: Generates production Bash scripts with a visual timeline UI, state-
7
7
 
8
8
  Generate production-ready Bash scripts for DevOps and CLI workflows. Enforce strict formatting with a visual timeline UI and state-based interactivity.
9
9
 
10
- Load `${CLAUDE_SKILL_DIR}/references/patterns.md` for the timeline lifecycle, logging, interactive prompt, help-screen, and full-script code templates. `${CLAUDE_SKILL_DIR}` expands to this skill's own directory, so the path resolves from any project. Copy those definitions verbatim, keeping only the colors and functions the script actually uses.
10
+ Load `${CLAUDE_SKILL_DIR}/references/patterns.md` for the timeline lifecycle, logging, help-screen, and full-script code templates, and `${CLAUDE_SKILL_DIR}/references/prompts.md` for the `ask()` and `select_option()` templates when the script prompts. `${CLAUDE_SKILL_DIR}` expands to this skill's own directory, so the path resolves from any project. Copy those definitions verbatim, keeping only the colors and functions the script actually uses.
11
11
 
12
12
  ## Guards
13
13
 
@@ -91,105 +91,6 @@ Renders as:
91
91
  └
92
92
  ```
93
93
 
94
- ## Interactive prompts
95
-
96
- Both prompts transition `◆` to `◇`, guard against non-TTY stdin with `[ -t 0 ]`, and write UI to stderr.
97
-
98
- ```bash
99
- ask() {
100
- local prompt_text=$1
101
- local var_name=$2
102
- local default_val=$3
103
- local input=""
104
- local char
105
- local display_default=""
106
- if [ -n "$default_val" ]; then
107
- display_default=" (${default_val})"
108
- fi
109
- if [ ! -t 0 ]; then
110
- log_error "${prompt_text} requires a TTY"
111
- fi
112
- echo -e "${GREY}│${NC}" >&2
113
- echo -ne "${GREEN}◆${NC} ${prompt_text}${display_default} " >&2
114
- while IFS= read -r -s -n1 char; do
115
- if [[ $char == $'\x1b' ]]; then
116
- read -rsn2 -t 0.001 _ || true
117
- echo -ne "\r\033[K" >&2
118
- echo -e "${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
119
- exit 1
120
- elif [[ $char == $'\x7f' || $char == $'\x08' ]]; then
121
- if [ -n "$input" ]; then
122
- input="${input%?}"
123
- echo -ne "\b \b" >&2
124
- fi
125
- elif [[ -z "$char" ]]; then
126
- break
127
- else
128
- input+="$char"
129
- echo -n "$char" >&2
130
- fi
131
- done
132
- [ -z "$input" ] && input="$default_val"
133
- echo -ne "\r\033[K" >&2
134
- echo -e "${GREY}◇${NC} ${prompt_text} ${WHITE}${input}${NC}" >&2
135
- export "$var_name"="$input"
136
- }
137
- ```
138
-
139
- ```bash
140
- select_option() {
141
- local prompt_text=$1
142
- shift
143
- local options=("$@")
144
- local cur=0
145
- local count=${#options[@]}
146
-
147
- if [ ! -t 0 ]; then
148
- log_error "${prompt_text} requires a TTY"
149
- fi
150
-
151
- echo -ne "${GREY}│${NC}\n${GREEN}◆${NC} ${prompt_text}\n" >&2
152
-
153
- while true; do
154
- for i in "${!options[@]}"; do
155
- if [ "$i" -eq "$cur" ]; then
156
- echo -e "${GREY}│${NC} ${GREEN}❯ ${options[$i]}${NC}" >&2
157
- else
158
- echo -e "${GREY}│${NC} ${GREY}${options[$i]}${NC}" >&2
159
- fi
160
- done
161
-
162
- read -rsn1 key
163
- case "$key" in
164
- $'\x1b')
165
- if read -rsn2 -t 0.001 key_seq; then
166
- if [[ "$key_seq" == "[A" ]]; then cur=$(( (cur - 1 + count) % count )); fi
167
- if [[ "$key_seq" == "[B" ]]; then cur=$(( (cur + 1) % count )); fi
168
- else
169
- echo -en "\033[$((count + 1))A\033[J" >&2
170
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
171
- exit 1
172
- fi
173
- ;;
174
- "k") cur=$(( (cur - 1 + count) % count ));;
175
- "j") cur=$(( (cur + 1) % count ));;
176
- "q")
177
- echo -en "\033[$((count + 1))A\033[J" >&2
178
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
179
- exit 1
180
- ;;
181
- "") break ;;
182
- esac
183
-
184
- echo -en "\033[${count}A" >&2
185
- done
186
-
187
- echo -en "\033[$((count + 1))A\033[J" >&2
188
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${WHITE}${options[$cur]}${NC}" >&2
189
- SELECTED_OPTION="${options[$cur]}"
190
- }
191
- ```
192
-
193
94
  ## Help system
194
95
 
195
96
  ```bash