claude-toolkit 0.9.0 → 0.11.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/CHANGELOG.md +22 -0
- package/README.md +27 -32
- package/bin/cli.ts +198 -162
- package/bin/postinstall.mjs +69 -0
- package/core/agents/ct-code-reviewer.md +5 -4
- package/core/agents/ct-github-workflow.md +28 -15
- package/core/commands/ct/code-quality.md +22 -2
- package/core/commands/ct/onboard.md +8 -3
- package/core/commands/ct/pr-review.md +22 -88
- package/core/commands/ct/pr-summary.md +6 -3
- package/core/commands/ct/ticket.md +20 -3
- package/core/hooks/{skill-eval.js → skill-eval.cjs} +103 -41
- package/core/hooks/skill-rules.schema.json +4 -0
- package/core/skills/ct-systematic-debugging/SKILL.md +1 -1
- package/core/skills/ct-testing-patterns/SKILL.md +5 -7
- package/core/skills/ct-typescript-conventions/SKILL.md +15 -11
- package/core/skills/ct-verification-before-completion/SKILL.md +2 -1
- package/docs/README.md +4 -0
- package/docs/commands/code-quality.md +2 -8
- package/docs/commands/onboard.md +2 -11
- package/docs/commands/pr-review.md +2 -13
- package/docs/commands/pr-summary.md +2 -2
- package/docs/commands/proto-check.md +4 -4
- package/docs/commands/ticket.md +2 -2
- package/docs/skills/systematic-debugging.md +1 -1
- package/docs/skills/testing-patterns.md +78 -56
- package/docs/skills/typescript-conventions.md +23 -10
- package/docs/skills/verification-before-completion.md +1 -1
- package/package.json +3 -1
- package/src/generator.ts +84 -12
- package/src/utils.ts +8 -15
- package/{core/commands/ct → stacks/protobuf/commands}/proto-check.md +11 -2
- package/templates/skill-rules.base.json +1 -29
- package/core/hooks/skill-eval.sh +0 -35
package/src/generator.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { copyFile as fsCopyFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile as fsCopyFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import type { ClaudeToolkitConfig, ResolvedConfig, StackPack } from "./types.js";
|
|
4
|
-
import { copyDir, exists, readJson, writeFileEnsureDir } from "./utils.js";
|
|
4
|
+
import { copyDir, exists, readJson, removePath, writeFileEnsureDir } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
const TOOLKIT_ROOT = resolve(import.meta.dirname, "..");
|
|
7
7
|
|
|
@@ -48,11 +48,56 @@ async function resolveConfig(config: ClaudeToolkitConfig): Promise<ResolvedConfi
|
|
|
48
48
|
return { config, skills, directoryMappings: allMappings, hooks, stacks };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Remove only `ct-` prefixed entries in a directory (preserves user-authored files). */
|
|
52
|
+
async function removeCtPrefixed(dir: string): Promise<void> {
|
|
53
|
+
if (!exists(dir)) return;
|
|
54
|
+
const entries = await readdir(dir);
|
|
55
|
+
await Promise.all(
|
|
56
|
+
entries.filter((e) => e.startsWith("ct-")).map((e) => removePath(join(dir, e))),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remove toolkit-generated artifacts from .claude/ before a rebuild, so a stack
|
|
62
|
+
* removed from the config doesn't leave an orphaned skill behind.
|
|
63
|
+
*
|
|
64
|
+
* Scoped to what the toolkit owns: `ct-` prefixed skills/agents, the generated
|
|
65
|
+
* skills index, the `ct/` command namespace, and the toolkit's own hook files.
|
|
66
|
+
* `.claude/skills`, `/agents`, and `/hooks` are shared Claude Code namespaces, so
|
|
67
|
+
* any user-authored files there — and the user files at the .claude root
|
|
68
|
+
* (settings.local.json, user-team-info.json, tasks/) — are preserved.
|
|
69
|
+
* (settings.json and .gitignore are single generated files, overwritten by generate.)
|
|
70
|
+
*/
|
|
71
|
+
async function removeGenerated(claudeDir: string): Promise<void> {
|
|
72
|
+
const coreHooks = join(TOOLKIT_ROOT, "core", "hooks");
|
|
73
|
+
const hookFiles = exists(coreHooks) ? await readdir(coreHooks) : [];
|
|
74
|
+
await Promise.all([
|
|
75
|
+
removeCtPrefixed(join(claudeDir, "skills")),
|
|
76
|
+
removeCtPrefixed(join(claudeDir, "agents")),
|
|
77
|
+
removePath(join(claudeDir, "skills", "README.md")),
|
|
78
|
+
removePath(join(claudeDir, "commands", "ct")),
|
|
79
|
+
// Toolkit hook files (copied from core/hooks) + the generated skill-rules.json.
|
|
80
|
+
...hookFiles.map((f) => removePath(join(claudeDir, "hooks", f))),
|
|
81
|
+
removePath(join(claudeDir, "hooks", "skill-rules.json")),
|
|
82
|
+
// Stale files from prior installs: skill-eval.js was renamed to .cjs, and the
|
|
83
|
+
// skill-eval.sh wrapper was dropped in favour of exec-form node registration.
|
|
84
|
+
removePath(join(claudeDir, "hooks", "skill-eval.js")),
|
|
85
|
+
removePath(join(claudeDir, "hooks", "skill-eval.sh")),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
51
89
|
/** Generate the .claude/ directory from resolved config */
|
|
52
|
-
export async function generate(
|
|
90
|
+
export async function generate(
|
|
91
|
+
projectDir: string,
|
|
92
|
+
config: ClaudeToolkitConfig,
|
|
93
|
+
options: { quiet?: boolean; scaffold?: boolean } = {},
|
|
94
|
+
): Promise<void> {
|
|
53
95
|
const resolved = await resolveConfig(config);
|
|
54
96
|
const claudeDir = join(projectDir, ".claude");
|
|
55
97
|
|
|
98
|
+
// 0. Clean previously-generated output so removed stacks don't leave stale skills.
|
|
99
|
+
await removeGenerated(claudeDir);
|
|
100
|
+
|
|
56
101
|
// 1. Copy core hooks
|
|
57
102
|
await copyDir(join(TOOLKIT_ROOT, "core", "hooks"), join(claudeDir, "hooks"));
|
|
58
103
|
|
|
@@ -73,6 +118,16 @@ export async function generate(projectDir: string, config: ClaudeToolkitConfig):
|
|
|
73
118
|
}
|
|
74
119
|
}
|
|
75
120
|
|
|
121
|
+
// 5b. Copy stack-specific commands into the ct/ namespace. Only installs when
|
|
122
|
+
// the owning stack is active; removeGenerated() already deletes the whole
|
|
123
|
+
// .claude/commands/ct dir each rebuild, so a removed stack leaves no orphan.
|
|
124
|
+
for (const stackName of config.stacks) {
|
|
125
|
+
const stackCommandsDir = join(TOOLKIT_ROOT, "stacks", stackName, "commands");
|
|
126
|
+
if (exists(stackCommandsDir)) {
|
|
127
|
+
await copyDir(stackCommandsDir, join(claudeDir, "commands", "ct"));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
76
131
|
// 6. Generate skill-rules.json
|
|
77
132
|
await generateSkillRules(claudeDir, resolved);
|
|
78
133
|
|
|
@@ -92,18 +147,30 @@ export async function generate(projectDir: string, config: ClaudeToolkitConfig):
|
|
|
92
147
|
"# Task-specific context",
|
|
93
148
|
"tasks/",
|
|
94
149
|
"",
|
|
150
|
+
"# Toolkit regeneration marker",
|
|
151
|
+
".toolkit-version",
|
|
152
|
+
"",
|
|
95
153
|
].join("\n"),
|
|
96
154
|
);
|
|
97
155
|
|
|
98
|
-
// 9. Scaffold base configs
|
|
99
|
-
|
|
156
|
+
// 9. Scaffold base configs into the project root (committed files). Skipped for
|
|
157
|
+
// automatic/postinstall regeneration so an install never writes committed files.
|
|
158
|
+
if (options.scaffold !== false) {
|
|
159
|
+
await scaffoldConfigs(projectDir, resolved, options.quiet);
|
|
160
|
+
}
|
|
100
161
|
|
|
101
162
|
// 10. Generate skills README
|
|
102
163
|
await generateSkillsReadme(claudeDir, resolved);
|
|
103
164
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
165
|
+
// Record the toolkit version that produced this output — drives auto-regen on update.
|
|
166
|
+
const { version } = await readJson<{ version: string }>(join(TOOLKIT_ROOT, "package.json"));
|
|
167
|
+
await writeFileEnsureDir(join(claudeDir, ".toolkit-version"), `${version}\n`);
|
|
168
|
+
|
|
169
|
+
if (!options.quiet) {
|
|
170
|
+
console.log(
|
|
171
|
+
`Generated .claude/ with ${resolved.stacks.length} stack(s) and ${resolved.skills.length} core skills`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
107
174
|
}
|
|
108
175
|
|
|
109
176
|
/** Generate skill-rules.json from resolved config */
|
|
@@ -239,7 +306,8 @@ async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Pr
|
|
|
239
306
|
hooks: [
|
|
240
307
|
{
|
|
241
308
|
type: "command",
|
|
242
|
-
|
|
309
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Claude Code substitutes ${CLAUDE_PROJECT_DIR} at hook-run time; this must be a literal placeholder string.
|
|
310
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/skill-eval.cjs"',
|
|
243
311
|
timeout: 5,
|
|
244
312
|
},
|
|
245
313
|
],
|
|
@@ -271,7 +339,11 @@ async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Pr
|
|
|
271
339
|
}
|
|
272
340
|
|
|
273
341
|
/** Scaffold base config files (biome.json, tsconfig.json) into the project */
|
|
274
|
-
async function scaffoldConfigs(
|
|
342
|
+
async function scaffoldConfigs(
|
|
343
|
+
projectDir: string,
|
|
344
|
+
resolved: ResolvedConfig,
|
|
345
|
+
quiet = false,
|
|
346
|
+
): Promise<void> {
|
|
275
347
|
if (resolved.config.scaffoldConfigs === false) return;
|
|
276
348
|
|
|
277
349
|
const configsDir = join(TOOLKIT_ROOT, "templates", "configs");
|
|
@@ -283,10 +355,10 @@ async function scaffoldConfigs(projectDir: string, resolved: ResolvedConfig): Pr
|
|
|
283
355
|
for (const { src, dest } of configs) {
|
|
284
356
|
const destPath = join(projectDir, dest);
|
|
285
357
|
if (exists(destPath)) {
|
|
286
|
-
console.log(` Skipped ${dest} (already exists)`);
|
|
358
|
+
if (!quiet) console.log(` Skipped ${dest} (already exists)`);
|
|
287
359
|
} else {
|
|
288
360
|
await fsCopyFile(join(configsDir, src), destPath);
|
|
289
|
-
console.log(` Scaffolded ${dest}`);
|
|
361
|
+
if (!quiet) console.log(` Scaffolded ${dest}`);
|
|
290
362
|
}
|
|
291
363
|
}
|
|
292
364
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
copyFile,
|
|
4
|
-
mkdir,
|
|
5
|
-
readdir,
|
|
6
|
-
readFile,
|
|
7
|
-
writeFile,
|
|
8
|
-
} from "node:fs/promises";
|
|
2
|
+
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
3
|
import { dirname, join } from "node:path";
|
|
10
4
|
|
|
11
5
|
/** Recursively copy a directory */
|
|
@@ -24,11 +18,13 @@ export async function copyDir(src: string, dest: string): Promise<void> {
|
|
|
24
18
|
}
|
|
25
19
|
}
|
|
26
20
|
|
|
21
|
+
/** Remove a file or directory recursively. No-op if the path doesn't exist. */
|
|
22
|
+
export async function removePath(path: string): Promise<void> {
|
|
23
|
+
await rm(path, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
27
26
|
/** Write a file, creating parent directories as needed */
|
|
28
|
-
export async function writeFileEnsureDir(
|
|
29
|
-
filePath: string,
|
|
30
|
-
content: string,
|
|
31
|
-
): Promise<void> {
|
|
27
|
+
export async function writeFileEnsureDir(filePath: string, content: string): Promise<void> {
|
|
32
28
|
await mkdir(dirname(filePath), { recursive: true });
|
|
33
29
|
await writeFile(filePath, content, "utf-8");
|
|
34
30
|
}
|
|
@@ -45,9 +41,6 @@ export function exists(filePath: string): boolean {
|
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
/** Simple template replacement: {{key}} → value */
|
|
48
|
-
export function renderTemplate(
|
|
49
|
-
template: string,
|
|
50
|
-
vars: Record<string, string>,
|
|
51
|
-
): string {
|
|
44
|
+
export function renderTemplate(template: string, vars: Record<string, string>): string {
|
|
52
45
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
53
46
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Validate protobuf definitions and check for breaking changes
|
|
3
3
|
allowed-tools:
|
|
4
|
-
- Bash
|
|
4
|
+
- Bash(buf:*)
|
|
5
|
+
- Bash(protoc:*)
|
|
6
|
+
- Bash(npm run:*)
|
|
7
|
+
- Bash(pnpm:*)
|
|
8
|
+
- Bash(bun run:*)
|
|
9
|
+
- Bash(yarn:*)
|
|
10
|
+
- Bash(tsc:*)
|
|
11
|
+
- Bash(cargo build:*)
|
|
12
|
+
- Bash(cargo check:*)
|
|
13
|
+
- Bash(go build:*)
|
|
5
14
|
- Read
|
|
6
15
|
- Glob
|
|
7
16
|
- Grep
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
"(?:red.green.refactor|tdd)"
|
|
41
41
|
],
|
|
42
42
|
"contentPatterns": ["describe\\(", "it\\(", "expect\\(", "vi\\.mock", "jest\\.mock"]
|
|
43
|
-
}
|
|
44
|
-
"excludePatterns": ["e2e", "end-to-end"]
|
|
43
|
+
}
|
|
45
44
|
},
|
|
46
45
|
"ct-typescript-conventions": {
|
|
47
46
|
"description": "TypeScript strict mode and conventions",
|
|
@@ -66,33 +65,6 @@
|
|
|
66
65
|
],
|
|
67
66
|
"contextPatterns": ["before claiming", "make sure", "double check"]
|
|
68
67
|
}
|
|
69
|
-
},
|
|
70
|
-
"ct-github-actions": {
|
|
71
|
-
"description": "GitHub Actions CI/CD workflow patterns",
|
|
72
|
-
"priority": 6,
|
|
73
|
-
"triggers": {
|
|
74
|
-
"keywords": ["github action", "github workflow", "workflow", "ci/cd", "cicd", "pipeline"],
|
|
75
|
-
"keywordPatterns": ["github.action", "ci/?cd", "\\bworkflow\\b"],
|
|
76
|
-
"pathPatterns": [".github/workflows/**", ".github/**/*.yml"],
|
|
77
|
-
"intentPatterns": [
|
|
78
|
-
"(?:create|add|update).*(?:workflow|action|pipeline)",
|
|
79
|
-
"(?:ci|cd).*(?:pipeline|workflow)"
|
|
80
|
-
],
|
|
81
|
-
"contentPatterns": ["runs-on:", "uses:", "steps:"]
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
"ct-code-review": {
|
|
85
|
-
"description": "Handle code review feedback",
|
|
86
|
-
"priority": 4,
|
|
87
|
-
"triggers": {
|
|
88
|
-
"keywords": ["code review", "pr feedback", "review comment"],
|
|
89
|
-
"keywordPatterns": ["code.review", "pr.feedback", "review.comment"],
|
|
90
|
-
"intentPatterns": [
|
|
91
|
-
"(?:respond|address|handle).*(?:review|feedback|comment)",
|
|
92
|
-
"(?:reviewer).*(?:said|suggested|asked)"
|
|
93
|
-
],
|
|
94
|
-
"contextPatterns": ["from the reviewer", "in the pr", "review feedback"]
|
|
95
|
-
}
|
|
96
68
|
}
|
|
97
69
|
}
|
|
98
70
|
}
|
package/core/hooks/skill-eval.sh
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Skill Evaluation Hook v2.0
|
|
3
|
-
# Wrapper script that delegates to the Node.js evaluation engine
|
|
4
|
-
#
|
|
5
|
-
# This hook runs on UserPromptSubmit and analyzes the prompt for:
|
|
6
|
-
# - Keywords and patterns indicating skill relevance
|
|
7
|
-
# - File paths mentioned in the prompt
|
|
8
|
-
# - Intent patterns (what the user wants to do)
|
|
9
|
-
# - Directory mappings (what directories map to which skills)
|
|
10
|
-
#
|
|
11
|
-
# Configuration is in skill-rules.json
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
NODE_SCRIPT="$SCRIPT_DIR/skill-eval.js"
|
|
15
|
-
|
|
16
|
-
# Check if Node.js is available (bun also works)
|
|
17
|
-
if command -v bun &>/dev/null; then
|
|
18
|
-
RUNTIME="bun"
|
|
19
|
-
elif command -v node &>/dev/null; then
|
|
20
|
-
RUNTIME="node"
|
|
21
|
-
else
|
|
22
|
-
# Fallback: exit silently if no runtime found
|
|
23
|
-
exit 0
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# Check if the Node script exists
|
|
27
|
-
if [[ ! -f "$NODE_SCRIPT" ]]; then
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Pipe stdin to the evaluation script (suppress stderr noise from shell init)
|
|
32
|
-
cat | $RUNTIME "$NODE_SCRIPT" 2>/dev/null
|
|
33
|
-
|
|
34
|
-
# Always exit 0 to allow the prompt through
|
|
35
|
-
exit 0
|