@erclx/canon 4.67.0 → 4.69.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/{claude-autoship → auto-ship}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-autoship → auto-ship}/SKILL.md +36 -36
- package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
- package/claude/skills/canon-cli/SKILL.md +2 -2
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback-triage/SKILL.md +3 -3
- package/claude/skills/canon-operator/REQUIREMENT.md +1 -1
- package/claude/skills/canon-operator/SKILL.md +4 -4
- package/claude/skills/canon-rollout/REQUIREMENT.md +6 -6
- package/claude/skills/canon-rollout/SKILL.md +7 -7
- package/claude/skills/{claude-design-extract → design-extract}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-design-extract → design-extract}/SKILL.md +1 -1
- package/claude/skills/{claude-docs → docs-fold}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-docs → docs-fold}/SKILL.md +14 -14
- package/claude/skills/{claude-docs → docs-fold}/references/anchor-sweep.md +1 -1
- package/claude/skills/{claude-docs → docs-fold}/references/wireframe-sweep.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +3 -3
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -4
- package/claude/skills/draft-and-pick/SKILL.md +6 -6
- package/claude/skills/draft-context/REQUIREMENT.md +2 -2
- package/claude/skills/draft-context/SKILL.md +3 -3
- package/claude/skills/{claude-diagram → draft-diagram}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-diagram → draft-diagram}/SKILL.md +3 -3
- package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +3 -3
- package/claude/skills/git-ship/REQUIREMENT.md +2 -2
- package/claude/skills/git-ship/SKILL.md +8 -8
- package/claude/skills/git-worktree/REQUIREMENT.md +2 -2
- package/claude/skills/git-worktree/SKILL.md +3 -3
- package/claude/skills/identity/REQUIREMENT.md +2 -2
- package/claude/skills/identity/SKILL.md +3 -3
- package/claude/skills/{claude-markdown-propose → markdown-propose}/REQUIREMENT.md +8 -8
- package/claude/skills/{claude-markdown-propose → markdown-propose}/SKILL.md +5 -5
- package/claude/skills/{claude-markdown-propose → markdown-propose}/references/format.md +1 -1
- package/claude/skills/{claude-memory-capture → memory-capture}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-memory-capture → memory-capture}/SKILL.md +13 -13
- package/claude/skills/{claude-memory-review → memory-review}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-memory-review → memory-review}/SKILL.md +10 -10
- package/claude/skills/migration-context/SKILL.md +1 -1
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +1 -1
- package/claude/skills/migration-superseded/REQUIREMENT.md +1 -1
- package/claude/skills/{claude-feature → plan-feature}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-feature → plan-feature}/SKILL.md +4 -4
- package/claude/skills/{claude-groundwork → plan-groundwork}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-groundwork → plan-groundwork}/SKILL.md +8 -8
- package/claude/skills/{claude-intake → plan-intake}/REQUIREMENT.md +6 -6
- package/claude/skills/{claude-intake → plan-intake}/SKILL.md +10 -10
- package/claude/skills/{claude-intake-answer → plan-intake-answer}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-intake-answer → plan-intake-answer}/SKILL.md +5 -5
- package/claude/skills/{claude-address-review → review-address}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-address-review → review-address}/SKILL.md +6 -6
- package/claude/skills/{claude-address-review → review-address}/references/rebase-conflicts.md +1 -1
- package/claude/skills/{claude-review → review-branch}/REQUIREMENT.md +4 -4
- package/claude/skills/{claude-review → review-branch}/SKILL.md +4 -4
- package/claude/skills/{claude-pr-review → review-pr}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-pr-review → review-pr}/SKILL.md +11 -11
- package/claude/skills/{claude-orchestrate → role-orchestrator}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-orchestrate → role-orchestrator}/SKILL.md +20 -20
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-dispatch.md +30 -30
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-handoff.md +2 -2
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-parked.md +8 -8
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-poll.md +8 -8
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-resume.md +1 -1
- package/claude/skills/{claude-orchestrate → role-orchestrator}/references/orchestrator-sweep.md +1 -1
- package/claude/skills/{claude-orchestrate → role-orchestrator}/scripts/poll.sh +6 -6
- package/claude/skills/{claude-planner → role-planner}/REQUIREMENT.md +12 -12
- package/claude/skills/{claude-planner → role-planner}/SKILL.md +4 -4
- package/claude/skills/{claude-worker → role-worker}/REQUIREMENT.md +8 -8
- package/claude/skills/{claude-worker → role-worker}/SKILL.md +6 -6
- package/claude/skills/{claude-seed-sync → seed-sync}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-seed-sync → seed-sync}/SKILL.md +3 -3
- package/claude/skills/session-map/REQUIREMENT.md +2 -2
- package/claude/skills/session-map/SKILL.md +2 -2
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-resume/SKILL.md +2 -2
- package/claude/skills/{claude-worktree → session-worktree}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-worktree → session-worktree}/SKILL.md +6 -6
- package/claude/skills/setup-plugins/references/plugin-catalog.md +1 -1
- package/claude/skills/{claude-standards-audit → standards-audit}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-standards-audit → standards-audit}/SKILL.md +2 -2
- package/claude/skills/systematic-debugging/REQUIREMENT.md +1 -1
- package/claude/skills/{claude-tasks → task-board}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-tasks → task-board}/SKILL.md +7 -7
- package/claude/skills/{claude-teach → teach-workspace}/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-teach → teach-workspace}/SKILL.md +4 -4
- package/claude/skills/test-first/REQUIREMENT.md +2 -2
- package/claude/skills/{claude-ui-test → ui-test}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-ui-test → ui-test}/SKILL.md +3 -3
- package/claude/skills/{claude-ux-audit → ux-audit}/REQUIREMENT.md +6 -6
- package/claude/skills/{claude-ux-audit → ux-audit}/SKILL.md +5 -5
- package/claude/skills/{claude-ux-measure → ux-measure}/REQUIREMENT.md +5 -5
- package/claude/skills/{claude-ux-measure → ux-measure}/SKILL.md +5 -5
- package/docs/agents/commands.md +1 -0
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/restated.md +1 -1
- package/docs/agents/review-classification.md +1 -1
- package/docs/agents/sandbox.md +13 -10
- package/docs/agents/sessions.md +1 -1
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/targets.md +1 -1
- package/docs/agents/tasks.md +4 -4
- package/docs/agents/teach.md +1 -1
- package/docs/target-projects.md +10 -10
- package/docs/workflow/ai-workflow.md +84 -84
- package/docs/workflow/operating-model.md +17 -17
- package/docs/workflow/visual-design-workflow.md +6 -6
- package/governance/rules/core/045-memory.md +1 -1
- package/governance/rules/core/085-worktrees.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-agent-fixture.sh +1 -1
- package/scripts/core/regen-hero.sh +7 -7
- package/scripts/lib/sandbox-dispatch.sh +8 -0
- package/snippets/claude/decision-memo.md +1 -1
- package/src/autoship/paths.ts +1 -1
- package/src/claude/cases/all.ts +2 -2
- package/src/claude/cases/{claude-workflow.ts → workflow.ts} +33 -33
- package/src/claude/plugin-update.ts +48 -0
- package/src/commands/claude.ts +281 -1
- package/src/commands/feedback.ts +15 -5
- package/src/commands/gate.ts +3 -1
- package/src/commands/sandbox.ts +13 -4
- package/src/commands/sync.ts +3 -3
- package/src/design/components.ts +12 -0
- package/src/design/tokens.ts +1 -1
- package/src/gate/measures.ts +47 -1
- package/src/gov/restated.ts +2 -2
- package/src/markdown/structure.ts +1 -1
- package/src/migrate/rename.ts +4 -3
- package/src/pr/bijection.ts +1 -1
- package/src/pr/paths.ts +2 -2
- package/src/sandbox/expect.ts +26 -1
- package/src/shipped/references.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +4 -4
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/src/teach/nav.ts +97 -3
- package/standards/glossary.md +8 -0
- package/standards/groundwork.md +1 -1
- package/standards/snippets.md +1 -1
- package/standards/tasks.md +3 -3
- package/standards/teach.md +2 -2
- package/tooling/base/configs/.husky/post-merge +3 -3
- package/tooling/claude/reference.md +5 -5
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
- /package/claude/skills/{claude-memory-review → memory-review}/references/receipt-format.md +0 -0
- /package/claude/skills/{claude-orchestrate → role-orchestrator}/scripts/watch.sh +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/lesson-craft.md +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/pedagogy.md +0 -0
- /package/claude/skills/{claude-teach → teach-workspace}/references/promotion.md +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SkillCase } from '@/claude/skills-rank'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The session workflow skills: feature planning, review, and the artifacts
|
|
5
|
+
* that coordinate work across sessions.
|
|
6
6
|
*
|
|
7
7
|
* Every positive is phrased away from its skill's own quoted trigger, since a
|
|
8
8
|
* verbatim trigger passes by construction and proves nothing about a prompt in
|
|
@@ -10,33 +10,33 @@ import type { SkillCase } from '@/claude/skills-rank'
|
|
|
10
10
|
* an explicit `Do NOT` boundary against each other, since a boundary in a
|
|
11
11
|
* body does not reach the field a router reads.
|
|
12
12
|
*/
|
|
13
|
-
export const
|
|
13
|
+
export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
14
14
|
{
|
|
15
15
|
prompt: 'Go fix everything the reviewer flagged on my open PR.',
|
|
16
|
-
expect: '
|
|
16
|
+
expect: 'review-address',
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
prompt:
|
|
20
20
|
'Run the whole implement, verify, review, and ship pipeline for the approved plan.',
|
|
21
|
-
expect: '
|
|
21
|
+
expect: 'auto-ship',
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
prompt: "Pull together a design system from what's already in the app.",
|
|
25
|
-
expect: '
|
|
25
|
+
expect: 'design-extract',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
prompt: 'Draw me a diagram of how the pieces of this system connect.',
|
|
29
|
-
expect: '
|
|
29
|
+
expect: 'draft-diagram',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
prompt:
|
|
33
33
|
'Bring the internal planning docs under .claude up to date with what we decided this session.',
|
|
34
|
-
expect: '
|
|
34
|
+
expect: 'docs-fold',
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
prompt:
|
|
38
38
|
'Sketch out a plan for adding this new capability before we touch any code.',
|
|
39
|
-
expect: '
|
|
39
|
+
expect: 'plan-feature',
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
prompt:
|
|
@@ -46,43 +46,43 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
46
46
|
{
|
|
47
47
|
prompt:
|
|
48
48
|
'We need to measure this properly before committing to an approach.',
|
|
49
|
-
expect: '
|
|
49
|
+
expect: 'plan-groundwork',
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
prompt: 'Take this pile of raw notes and turn it into filed items.',
|
|
53
|
-
expect: '
|
|
53
|
+
expect: 'plan-intake',
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
prompt: 'Walk me through the open intake items so I can decide on each.',
|
|
57
|
-
expect: '
|
|
57
|
+
expect: 'plan-intake-answer',
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
prompt:
|
|
61
61
|
"Draft a diff-based proposal for tightening this passage in the standard, don't just edit it.",
|
|
62
|
-
expect: '
|
|
62
|
+
expect: 'markdown-propose',
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
prompt: 'Pull the durable lessons out of this session before it ends.',
|
|
66
|
-
expect: '
|
|
66
|
+
expect: 'memory-capture',
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
prompt:
|
|
70
70
|
'Go through the memory folder and propose what to do with each entry.',
|
|
71
|
-
expect: '
|
|
71
|
+
expect: 'memory-review',
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
prompt:
|
|
75
75
|
'Take on the orchestrator role and coordinate the parallel feature builds.',
|
|
76
|
-
expect: '
|
|
76
|
+
expect: 'role-orchestrator',
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
prompt:
|
|
80
80
|
'I am writing the plan for a row another session will build. What may I write, and where do I read what is already in flight?',
|
|
81
|
-
expect: '
|
|
81
|
+
expect: 'role-planner',
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
prompt: 'Post a formal review with findings on that open pull request.',
|
|
85
|
-
expect: '
|
|
85
|
+
expect: 'review-pr',
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
prompt:
|
|
@@ -92,7 +92,7 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
92
92
|
{
|
|
93
93
|
prompt:
|
|
94
94
|
'Look over everything that changed on this branch for bugs and edge cases.',
|
|
95
|
-
expect: '
|
|
95
|
+
expect: 'review-branch',
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
prompt: 'Draft me a script with beats for a screencast recording.',
|
|
@@ -101,7 +101,7 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
101
101
|
{
|
|
102
102
|
prompt:
|
|
103
103
|
"Check whether my installed Claude seed docs have drifted from the toolkit's.",
|
|
104
|
-
expect: '
|
|
104
|
+
expect: 'seed-sync',
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
prompt: 'Turn this topic into a slide deck I can render.',
|
|
@@ -110,37 +110,37 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
110
110
|
{
|
|
111
111
|
prompt:
|
|
112
112
|
'Check whether the markdown I changed violates any authoring standards.',
|
|
113
|
-
expect: '
|
|
113
|
+
expect: 'standards-audit',
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
prompt: 'Open a new entry on the task board for this piece of work.',
|
|
117
|
-
expect: '
|
|
117
|
+
expect: 'task-board',
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
prompt: 'Open a learning workspace so I can study this topic properly.',
|
|
121
|
-
expect: '
|
|
121
|
+
expect: 'teach-workspace',
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
prompt: 'Generate the Playwright tests I need after this UI change.',
|
|
125
|
-
expect: '
|
|
125
|
+
expect: 'ui-test',
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
prompt:
|
|
129
129
|
'Look over the interface and tell me what feels unfinished or confusing.',
|
|
130
|
-
expect: '
|
|
130
|
+
expect: 'ux-audit',
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
prompt: 'Tell me the paint and layout cost of this page right now.',
|
|
134
|
-
expect: '
|
|
134
|
+
expect: 'ux-measure',
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
prompt:
|
|
138
138
|
'I am building this branch for another session. What am I on the hook for, and what is off limits?',
|
|
139
|
-
expect: '
|
|
139
|
+
expect: 'role-worker',
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
|
|
143
|
-
expect: '
|
|
143
|
+
expect: 'session-worktree',
|
|
144
144
|
},
|
|
145
145
|
|
|
146
146
|
// Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
|
|
@@ -151,23 +151,23 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
prompt: 'Sort through this brain dump and write it up as findings.',
|
|
154
|
-
expect: '
|
|
154
|
+
expect: 'plan-intake',
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
prompt:
|
|
158
158
|
"Go through what's still unanswered in the intake folder and decide.",
|
|
159
|
-
expect: '
|
|
159
|
+
expect: 'plan-intake-answer',
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
prompt: 'Check this markdown against the house style rules.',
|
|
163
|
-
expect: '
|
|
163
|
+
expect: 'standards-audit',
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
prompt: 'Find the rough, unfinished-feeling spots in this interface.',
|
|
167
|
-
expect: '
|
|
167
|
+
expect: 'ux-audit',
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
prompt: 'Tell me the render cost of this page in the browser.',
|
|
171
|
-
expect: '
|
|
171
|
+
expect: 'ux-measure',
|
|
172
172
|
},
|
|
173
173
|
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** One row of `claude plugin list --json`, the fields this module reads. */
|
|
2
|
+
export interface PluginInstall {
|
|
3
|
+
readonly id: string
|
|
4
|
+
readonly version: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The plugin's own name, out of a `plugin.json` buffer. `undefined` on
|
|
9
|
+
* anything that fails to parse or carries no string `name`, so a caller
|
|
10
|
+
* refuses rather than matching against `undefined@`.
|
|
11
|
+
*/
|
|
12
|
+
export function readPluginName(manifest: string): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(manifest) as { name?: unknown }
|
|
15
|
+
return typeof parsed.name === 'string' ? parsed.name : undefined
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type MatchResult =
|
|
22
|
+
| { readonly kind: 'matched'; readonly install: PluginInstall }
|
|
23
|
+
| { readonly kind: 'none' }
|
|
24
|
+
| { readonly kind: 'many'; readonly installs: readonly PluginInstall[] }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `id` is `<name>@<marketplace>`, so a name alone can match more than one row
|
|
28
|
+
* when the same plugin is installed from two marketplaces at once. Refusing
|
|
29
|
+
* that case rather than picking a row is the same move `readPluginName`
|
|
30
|
+
* makes: nothing here guesses at an ambiguity the caller can see plainly.
|
|
31
|
+
*/
|
|
32
|
+
export function matchInstall(
|
|
33
|
+
name: string,
|
|
34
|
+
installs: readonly PluginInstall[],
|
|
35
|
+
): MatchResult {
|
|
36
|
+
const prefix = `${name}@`
|
|
37
|
+
const matches = installs.filter((install) => install.id.startsWith(prefix))
|
|
38
|
+
|
|
39
|
+
if (matches.length === 0) return { kind: 'none' }
|
|
40
|
+
if (matches.length > 1) return { kind: 'many', installs: matches }
|
|
41
|
+
return { kind: 'matched', install: matches[0] as PluginInstall }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function updatedMessage(before: string, after: string): string {
|
|
45
|
+
return after === before
|
|
46
|
+
? `Reinstalled ${after}, unchanged.`
|
|
47
|
+
: `Updated ${before} to ${after}.`
|
|
48
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -3,7 +3,16 @@ import { chmod, readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
import type { Command } from 'commander'
|
|
6
|
+
import { execa } from 'execa'
|
|
7
|
+
import { singleLine } from '@/commands/upgrade'
|
|
8
|
+
import { gitEnv } from '@/git-env'
|
|
6
9
|
import { claudeChain, pendingEntries, planGitignore } from '@/claude/gitignore'
|
|
10
|
+
import {
|
|
11
|
+
matchInstall,
|
|
12
|
+
type PluginInstall,
|
|
13
|
+
readPluginName,
|
|
14
|
+
updatedMessage,
|
|
15
|
+
} from '@/claude/plugin-update'
|
|
7
16
|
import {
|
|
8
17
|
applySeeds,
|
|
9
18
|
countByScope,
|
|
@@ -90,6 +99,37 @@ interface SkillsDriftOptions {
|
|
|
90
99
|
readonly json?: boolean
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
interface PluginUpdateOptions {
|
|
103
|
+
readonly json?: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `no-claude` and `no-plugin` name permanent conditions on a machine that
|
|
108
|
+
* never carries the marketplace plugin at all, matching the `gh-missing` and
|
|
109
|
+
* `no-repository` reasons `.husky/post-merge`'s other two steps already stay
|
|
110
|
+
* quiet on forever rather than nagging a project that will never fix them.
|
|
111
|
+
* Every other reason is a real defect and prints.
|
|
112
|
+
*/
|
|
113
|
+
type PluginUpdateReason =
|
|
114
|
+
| 'no-claude'
|
|
115
|
+
| 'no-manifest'
|
|
116
|
+
| 'no-name'
|
|
117
|
+
| 'list-failed'
|
|
118
|
+
| 'no-plugin'
|
|
119
|
+
| 'ambiguous'
|
|
120
|
+
| 'update-failed'
|
|
121
|
+
| 'after-list-failed'
|
|
122
|
+
|
|
123
|
+
interface PluginUpdateRecord {
|
|
124
|
+
readonly root: string
|
|
125
|
+
readonly id?: string
|
|
126
|
+
readonly before?: string
|
|
127
|
+
readonly after?: string
|
|
128
|
+
readonly state: 'updated' | 'current' | 'refused'
|
|
129
|
+
readonly reason?: PluginUpdateReason
|
|
130
|
+
readonly message: string
|
|
131
|
+
}
|
|
132
|
+
|
|
93
133
|
interface SkillsReachOptions {
|
|
94
134
|
readonly json?: boolean
|
|
95
135
|
}
|
|
@@ -111,11 +151,24 @@ const SEEDED_FILES: readonly string[] = [
|
|
|
111
151
|
const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
|
|
112
152
|
const USER_DIR = join('tooling', 'claude', 'user')
|
|
113
153
|
const STATUSLINE = 'statusline-command.sh'
|
|
154
|
+
const PLUGIN_MANIFEST = join('claude', '.claude-plugin', 'plugin.json')
|
|
155
|
+
|
|
156
|
+
/** A local cache read, not a network round trip, matching reclaim.ts's own bound for `claude agents --json`. */
|
|
157
|
+
const PLUGIN_LIST_TIMEOUT_MS = 10_000
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `claude plugin update` fetches a marketplace archive over the network, so a
|
|
161
|
+
* stalled fetch should not hang the caller, which is a git hook on the
|
|
162
|
+
* ordinary path.
|
|
163
|
+
*/
|
|
164
|
+
const PLUGIN_UPDATE_TIMEOUT_MS = 60_000
|
|
114
165
|
|
|
115
166
|
export function register(program: Command): void {
|
|
116
167
|
const claude = program
|
|
117
168
|
.command('claude')
|
|
118
|
-
.description(
|
|
169
|
+
.description(
|
|
170
|
+
'Claude workflow (init, seeds, sync, setup, routing, plugin-update)',
|
|
171
|
+
)
|
|
119
172
|
.helpOption('-h, --help', 'Show this help message')
|
|
120
173
|
.addHelpText(
|
|
121
174
|
'after',
|
|
@@ -227,6 +280,39 @@ export function register(program: Command): void {
|
|
|
227
280
|
process.exitCode = runRouting(path, opts)
|
|
228
281
|
})
|
|
229
282
|
|
|
283
|
+
claude
|
|
284
|
+
.command('plugin-update')
|
|
285
|
+
.description(
|
|
286
|
+
'Update the installed marketplace plugin cache to match this CLI',
|
|
287
|
+
)
|
|
288
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
289
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
290
|
+
.addHelpText(
|
|
291
|
+
'after',
|
|
292
|
+
[
|
|
293
|
+
'',
|
|
294
|
+
'Mechanics:',
|
|
295
|
+
' Reads the plugin name out of claude/.claude-plugin/plugin.json,',
|
|
296
|
+
' matches it against an installed row from `claude plugin list',
|
|
297
|
+
' --json` by id prefix (<name>@), then runs `claude plugin update',
|
|
298
|
+
' <id> -y`. There is no --json on the update call itself, so the',
|
|
299
|
+
' version is read back off `claude plugin list --json` again and',
|
|
300
|
+
' compared to what it was before.',
|
|
301
|
+
'',
|
|
302
|
+
'Exit codes:',
|
|
303
|
+
' 0 current already, or the update ran',
|
|
304
|
+
' 1 refused, with the reason on stderr',
|
|
305
|
+
'',
|
|
306
|
+
'Examples:',
|
|
307
|
+
' canon claude plugin-update',
|
|
308
|
+
' canon claude plugin-update --json',
|
|
309
|
+
'',
|
|
310
|
+
].join('\n'),
|
|
311
|
+
)
|
|
312
|
+
.action(async (opts: PluginUpdateOptions) => {
|
|
313
|
+
process.exitCode = await runPluginUpdate(opts)
|
|
314
|
+
})
|
|
315
|
+
|
|
230
316
|
const skills = claude
|
|
231
317
|
.command('skills')
|
|
232
318
|
.description('Plugin skill catalog (list, audit, drift, reach, rank)')
|
|
@@ -830,6 +916,200 @@ function reportRouting(
|
|
|
830
916
|
)
|
|
831
917
|
}
|
|
832
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Reads the plugin name off the manifest, matches it against an installed row,
|
|
921
|
+
* runs the update, and reads the version back off the same list rather than
|
|
922
|
+
* trusting the update call's own report, since `claude plugin update` carries
|
|
923
|
+
* no `--json` to answer with. `canon upgrade` makes the identical move for the
|
|
924
|
+
* package managers it drives.
|
|
925
|
+
*/
|
|
926
|
+
async function runPluginUpdate(opts: PluginUpdateOptions): Promise<number> {
|
|
927
|
+
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
928
|
+
intro('canon claude plugin-update')
|
|
929
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
930
|
+
|
|
931
|
+
const manifestPath = join(PROJECT_ROOT, PLUGIN_MANIFEST)
|
|
932
|
+
let manifestText: string
|
|
933
|
+
try {
|
|
934
|
+
manifestText = await readFile(manifestPath, 'utf8')
|
|
935
|
+
} catch {
|
|
936
|
+
return refusePluginUpdate(
|
|
937
|
+
opts,
|
|
938
|
+
'no-manifest',
|
|
939
|
+
`No manifest at ${manifestPath}, so there is no plugin name to update.`,
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const name = readPluginName(manifestText)
|
|
944
|
+
if (name === undefined) {
|
|
945
|
+
return refusePluginUpdate(
|
|
946
|
+
opts,
|
|
947
|
+
'no-name',
|
|
948
|
+
`No name field in ${manifestPath}, so there is nothing to match against an installed plugin.`,
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
logStep('Manifest')
|
|
953
|
+
logInfo(`${name}, from ${manifestPath}`)
|
|
954
|
+
|
|
955
|
+
const before = await listPluginInstalls()
|
|
956
|
+
if (before.kind === 'missing') {
|
|
957
|
+
return refusePluginUpdate(
|
|
958
|
+
opts,
|
|
959
|
+
'no-claude',
|
|
960
|
+
'claude is not on PATH, so no installed plugin could be read.',
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
if (before.kind === 'failed') {
|
|
964
|
+
return refusePluginUpdate(
|
|
965
|
+
opts,
|
|
966
|
+
'list-failed',
|
|
967
|
+
`\`claude plugin list --json\` failed. ${before.detail}`,
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const match = matchInstall(name, before.installs)
|
|
972
|
+
if (match.kind === 'none') {
|
|
973
|
+
return refusePluginUpdate(
|
|
974
|
+
opts,
|
|
975
|
+
'no-plugin',
|
|
976
|
+
`No installed plugin carries the id prefix "${name}@". Install it with \`claude plugin install\` first.`,
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
if (match.kind === 'many') {
|
|
980
|
+
return refusePluginUpdate(
|
|
981
|
+
opts,
|
|
982
|
+
'ambiguous',
|
|
983
|
+
`Multiple installed plugins share the name "${name}": ${match.installs
|
|
984
|
+
.map((install) => install.id)
|
|
985
|
+
.join(', ')}. Refusing rather than picking one.`,
|
|
986
|
+
)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const { install } = match
|
|
990
|
+
logStep('Installed')
|
|
991
|
+
logInfo(`${install.id} ${install.version}`)
|
|
992
|
+
|
|
993
|
+
logStep('Updating')
|
|
994
|
+
const result = await execa('claude', ['plugin', 'update', install.id, '-y'], {
|
|
995
|
+
reject: false,
|
|
996
|
+
timeout: PLUGIN_UPDATE_TIMEOUT_MS,
|
|
997
|
+
env: gitEnv(),
|
|
998
|
+
extendEnv: false,
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
if (result.exitCode !== 0) {
|
|
1002
|
+
return refusePluginUpdate(
|
|
1003
|
+
opts,
|
|
1004
|
+
'update-failed',
|
|
1005
|
+
`\`claude plugin update ${install.id} -y\` exited ${result.exitCode}. ${(result.stderr || result.stdout).trim()}`,
|
|
1006
|
+
install.id,
|
|
1007
|
+
install.version,
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const after = await listPluginInstalls()
|
|
1012
|
+
if (after.kind !== 'ok') {
|
|
1013
|
+
return refusePluginUpdate(
|
|
1014
|
+
opts,
|
|
1015
|
+
'after-list-failed',
|
|
1016
|
+
'`claude plugin update` ran, but `claude plugin list --json` failed to read the version back.',
|
|
1017
|
+
install.id,
|
|
1018
|
+
install.version,
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const afterVersion =
|
|
1023
|
+
after.installs.find((row) => row.id === install.id)?.version ??
|
|
1024
|
+
install.version
|
|
1025
|
+
const state = afterVersion === install.version ? 'current' : 'updated'
|
|
1026
|
+
|
|
1027
|
+
logStep('Installed')
|
|
1028
|
+
logInfo(
|
|
1029
|
+
afterVersion === install.version
|
|
1030
|
+
? `${afterVersion}, unchanged`
|
|
1031
|
+
: `${install.version} to ${afterVersion}`,
|
|
1032
|
+
)
|
|
1033
|
+
outro()
|
|
1034
|
+
|
|
1035
|
+
emitPluginUpdate(opts, {
|
|
1036
|
+
root: PROJECT_ROOT,
|
|
1037
|
+
id: install.id,
|
|
1038
|
+
before: install.version,
|
|
1039
|
+
after: afterVersion,
|
|
1040
|
+
state,
|
|
1041
|
+
message: updatedMessage(install.version, afterVersion),
|
|
1042
|
+
})
|
|
1043
|
+
return 0
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
type ListInstallsResult =
|
|
1047
|
+
| { readonly kind: 'ok'; readonly installs: readonly PluginInstall[] }
|
|
1048
|
+
| { readonly kind: 'missing' }
|
|
1049
|
+
| { readonly kind: 'failed'; readonly detail: string }
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* `missing` separates a `claude` binary that is not on PATH from every other
|
|
1053
|
+
* failure, since only that condition is permanent enough for the hook to
|
|
1054
|
+
* silence forever. execa reports it as `ENOENT` on the result rather than by
|
|
1055
|
+
* throwing, because the call runs with `reject: false`.
|
|
1056
|
+
*/
|
|
1057
|
+
async function listPluginInstalls(): Promise<ListInstallsResult> {
|
|
1058
|
+
const result = await execa('claude', ['plugin', 'list', '--json'], {
|
|
1059
|
+
reject: false,
|
|
1060
|
+
timeout: PLUGIN_LIST_TIMEOUT_MS,
|
|
1061
|
+
env: gitEnv(),
|
|
1062
|
+
extendEnv: false,
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
if (result.code === 'ENOENT') return { kind: 'missing' }
|
|
1066
|
+
if (result.exitCode !== 0) {
|
|
1067
|
+
return { kind: 'failed', detail: (result.stderr || result.stdout).trim() }
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
return {
|
|
1072
|
+
kind: 'ok',
|
|
1073
|
+
installs: JSON.parse(result.stdout) as readonly PluginInstall[],
|
|
1074
|
+
}
|
|
1075
|
+
} catch {
|
|
1076
|
+
return {
|
|
1077
|
+
kind: 'failed',
|
|
1078
|
+
detail: '`claude plugin list --json` did not print valid JSON.',
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function refusePluginUpdate(
|
|
1084
|
+
opts: PluginUpdateOptions,
|
|
1085
|
+
reason: PluginUpdateReason,
|
|
1086
|
+
message: string,
|
|
1087
|
+
id?: string,
|
|
1088
|
+
before?: string,
|
|
1089
|
+
): number {
|
|
1090
|
+
outro()
|
|
1091
|
+
frameError(message)
|
|
1092
|
+
emitPluginUpdate(opts, {
|
|
1093
|
+
root: PROJECT_ROOT,
|
|
1094
|
+
...(id === undefined ? {} : { id }),
|
|
1095
|
+
...(before === undefined ? {} : { before }),
|
|
1096
|
+
state: 'refused',
|
|
1097
|
+
reason,
|
|
1098
|
+
message,
|
|
1099
|
+
})
|
|
1100
|
+
return 1
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function emitPluginUpdate(
|
|
1104
|
+
opts: PluginUpdateOptions,
|
|
1105
|
+
record: PluginUpdateRecord,
|
|
1106
|
+
): void {
|
|
1107
|
+
if (opts.json !== true) return
|
|
1108
|
+
process.stdout.write(
|
|
1109
|
+
`${JSON.stringify({ ...record, message: singleLine(record.message) })}\n`,
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
833
1113
|
/** What a reader does about the one way the corpus fails to build. */
|
|
834
1114
|
const REACH_REFUSALS: Record<ReachRefusal, string> = {
|
|
835
1115
|
'no-skills':
|
package/src/commands/feedback.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
missingField,
|
|
8
8
|
missingFieldMessage,
|
|
9
9
|
} from '@/commands/feedback-format'
|
|
10
|
-
import { PROJECT_ROOT } from '@/project-root'
|
|
10
|
+
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
|
|
11
11
|
import { creationRel } from '@/record-root'
|
|
12
12
|
import { createGithubIssue } from '@/github'
|
|
13
13
|
import { issueFailureMessage } from '@/github-format'
|
|
14
|
-
import { frameError, frameSuccess, palette } from '@/ui'
|
|
14
|
+
import { frameError, frameSuccess, logWarn, palette } from '@/ui'
|
|
15
15
|
|
|
16
16
|
function readStdin(): Promise<string> {
|
|
17
17
|
return new Promise((resolveStream, rejectStream) => {
|
|
@@ -45,7 +45,7 @@ function isToolkitSource(): boolean {
|
|
|
45
45
|
* hand, so each writes under its own name and the enclosing folder keeps the
|
|
46
46
|
* single ignore entry and the single backed-folder entry it already had.
|
|
47
47
|
*/
|
|
48
|
-
function writeLocal(body: string): string {
|
|
48
|
+
function writeLocal(body: string, mismatch: string | undefined): string {
|
|
49
49
|
// Resolved against the same root the write joins onto, so a checkout that has
|
|
50
50
|
// not migrated its records writes this beside the ones already there rather
|
|
51
51
|
// than opening a second root nothing reads.
|
|
@@ -55,7 +55,16 @@ function writeLocal(body: string): string {
|
|
|
55
55
|
const filename = `feedback-${deriveSlug(body)}-${timestamp()}.md`
|
|
56
56
|
const filePath = join(reviewDir, filename)
|
|
57
57
|
writeFileSync(filePath, `${body}\n`, 'utf8')
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
// Manual frame rather than `frameSuccess`, since the mismatch warning is a
|
|
60
|
+
// frame-interior line landing between the opener and the command title,
|
|
61
|
+
// matching the convention `#1587` set at its other six sites.
|
|
62
|
+
const { GREEN, GREY, NC, WHITE } = palette(process.stderr)
|
|
63
|
+
process.stderr.write(`${GREY}┌${NC}\n`)
|
|
64
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`${GREY}│${NC} ${WHITE}canon feedback${NC}\n${GREY}│${NC}\n${GREY}│${NC} ${GREEN}✓${NC} ${join(relativeDir, filename)}\n${GREY}└${NC}\n`,
|
|
67
|
+
)
|
|
59
68
|
return filePath
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -124,7 +133,8 @@ export function register(program: Command): void {
|
|
|
124
133
|
return
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
const
|
|
136
|
+
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
137
|
+
const filePath = writeLocal(body, mismatch)
|
|
128
138
|
process.stdout.write(`${filePath}\n`)
|
|
129
139
|
})
|
|
130
140
|
}
|
package/src/commands/gate.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
summarize,
|
|
13
13
|
} from '@/gate/sequencer'
|
|
14
14
|
import { STAGES } from '@/gate/stages'
|
|
15
|
-
import { PROJECT_ROOT } from '@/project-root'
|
|
15
|
+
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
|
|
16
16
|
import {
|
|
17
17
|
intro,
|
|
18
18
|
logError,
|
|
@@ -89,6 +89,7 @@ export function register(program: Command): void {
|
|
|
89
89
|
|
|
90
90
|
async function runGate(opts: RunCommandOptions): Promise<number> {
|
|
91
91
|
const root = PROJECT_ROOT
|
|
92
|
+
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
92
93
|
const emitJson = opts.json ?? false
|
|
93
94
|
const nested = opts.nested ?? false
|
|
94
95
|
const write = opts.write ?? true
|
|
@@ -102,6 +103,7 @@ async function runGate(opts: RunCommandOptions): Promise<number> {
|
|
|
102
103
|
? { scoped: false, files: [] }
|
|
103
104
|
: await collectChangedFiles(run)
|
|
104
105
|
if (!emitJson && changed.notice !== undefined) logWarn(changed.notice)
|
|
106
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
105
107
|
|
|
106
108
|
const ctx: GateContext = {
|
|
107
109
|
root,
|