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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +27 -32
  3. package/bin/cli.ts +198 -162
  4. package/bin/postinstall.mjs +69 -0
  5. package/core/agents/ct-code-reviewer.md +5 -4
  6. package/core/agents/ct-github-workflow.md +28 -15
  7. package/core/commands/ct/code-quality.md +22 -2
  8. package/core/commands/ct/onboard.md +8 -3
  9. package/core/commands/ct/pr-review.md +22 -88
  10. package/core/commands/ct/pr-summary.md +6 -3
  11. package/core/commands/ct/ticket.md +20 -3
  12. package/core/hooks/{skill-eval.js → skill-eval.cjs} +103 -41
  13. package/core/hooks/skill-rules.schema.json +4 -0
  14. package/core/skills/ct-systematic-debugging/SKILL.md +1 -1
  15. package/core/skills/ct-testing-patterns/SKILL.md +5 -7
  16. package/core/skills/ct-typescript-conventions/SKILL.md +15 -11
  17. package/core/skills/ct-verification-before-completion/SKILL.md +2 -1
  18. package/docs/README.md +4 -0
  19. package/docs/commands/code-quality.md +2 -8
  20. package/docs/commands/onboard.md +2 -11
  21. package/docs/commands/pr-review.md +2 -13
  22. package/docs/commands/pr-summary.md +2 -2
  23. package/docs/commands/proto-check.md +4 -4
  24. package/docs/commands/ticket.md +2 -2
  25. package/docs/skills/systematic-debugging.md +1 -1
  26. package/docs/skills/testing-patterns.md +78 -56
  27. package/docs/skills/typescript-conventions.md +23 -10
  28. package/docs/skills/verification-before-completion.md +1 -1
  29. package/package.json +3 -1
  30. package/src/generator.ts +84 -12
  31. package/src/utils.ts +8 -15
  32. package/{core/commands/ct → stacks/protobuf/commands}/proto-check.md +11 -2
  33. package/templates/skill-rules.base.json +1 -29
  34. 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(projectDir: string, config: ClaudeToolkitConfig): Promise<void> {
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
- await scaffoldConfigs(projectDir, resolved);
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
- console.log(
105
- `Generated .claude/ with ${resolved.stacks.length} stack(s) and ${resolved.skills.length} core skills`,
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
- command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/skill-eval.sh',
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(projectDir: string, resolved: ResolvedConfig): Promise<void> {
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: "Validate protobuf definitions and check for breaking changes"
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
  }
@@ -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