@erclx/canon 4.120.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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +29 -115
- package/claude/skills/auto-ship/references/draft-mark.md +30 -0
- package/claude/skills/auto-ship/references/failure-recovery.md +27 -0
- package/claude/skills/auto-ship/references/tier-failures.md +26 -0
- package/claude/skills/auto-ship/references/verb-absent.md +37 -0
- package/claude/skills/auto-ship/references/worktree-name.md +22 -0
- package/claude/skills/bash-script/SKILL.md +1 -1
- package/claude/skills/bash-script/references/patterns.md +0 -99
- package/claude/skills/bash-script/references/prompts.md +100 -0
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/create-snippet/SKILL.md +1 -1
- package/claude/skills/create-standard/SKILL.md +1 -1
- package/claude/skills/design-extract/SKILL.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -2
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-context/SKILL.md +1 -1
- package/claude/skills/draft-docs/SKILL.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +1 -1
- package/claude/skills/draft-readme/SKILL.md +1 -1
- package/claude/skills/draft-wiki/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +1 -1
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/memory-capture/SKILL.md +1 -1
- package/claude/skills/restate-plainly/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +4 -3
- package/claude/skills/review-branch/REQUIREMENT.md +1 -0
- package/claude/skills/review-branch/SKILL.md +5 -6
- package/claude/skills/review-craft/REQUIREMENT.md +43 -0
- package/claude/skills/review-craft/SKILL.md +50 -0
- package/claude/skills/review-craft/references/security.md +21 -0
- package/claude/skills/review-craft/references/ui.md +72 -0
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +1 -1
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/standards-audit/SKILL.md +1 -1
- package/claude/skills/write-human/REQUIREMENT.md +7 -4
- package/claude/skills/write-human/SKILL.md +4 -2
- package/claude/skills/write-human/references/machine-tells.md +1 -1
- package/claude/skills/write-human/references/source-material.md +5 -2
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/markdown-audit.md +13 -19
- package/docs/agents/pr-local.md +6 -3
- package/docs/target-projects.md +1 -1
- package/docs/workflow/ai-workflow.md +1 -0
- package/governance/rules/claude/500-prose.md +1 -1
- package/governance/rules/claude/501-markdown.md +3 -3
- package/package.json +1 -1
- package/src/autoship/paths.ts +3 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/commands/audits.ts +2 -2
- package/src/commands/markdown.ts +7 -9
- package/src/commands/pr.ts +11 -4
- package/src/gate/measures.ts +4 -4
- package/src/gate/stages.ts +0 -1
- package/src/markdown/bans.ts +11 -68
- package/src/markdown/gate.ts +1 -1
- package/src/markdown/scan.ts +7 -88
- package/standards/docs.md +1 -1
- package/standards/index.md +1 -1
- package/standards/markdown.md +5 -7
- package/standards/pr.md +1 -1
- package/standards/publish.md +1 -1
- package/standards/readme.md +2 -2
- package/tooling/base/configs/scripts/verify.sh +2 -2
- package/tooling/base/seeds/canon/context/development.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +5 -6
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
114
|
-
- `kind: 'unreadable'`. Report the `message` and continue to Step 5
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.`
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
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
|