@chris1807/claude-kit 2.1.16 → 2.1.18

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/bin/cli.js CHANGED
@@ -11,6 +11,8 @@ const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  const TEMPLATES_DIR = join(__dirname, '..', 'templates');
13
13
  const GLOBAL_CLAUDE_DIR = join(process.env.HOME || process.env.USERPROFILE, '.claude');
14
+ const PKG = await fs.readJson(join(__dirname, '..', 'package.json'));
15
+ const KIT_VERSION = PKG.version;
14
16
 
15
17
  // ============================================================================
16
18
  // CLI Arguments
@@ -110,6 +112,47 @@ async function installFile(src, dest, label) {
110
112
  return true;
111
113
  }
112
114
 
115
+ // ============================================================================
116
+ // Helper: additive merge for settings.json
117
+ // ============================================================================
118
+ // Top-level keys: keep user's value if present, add kit's only if missing.
119
+ // `hooks`: union of matcher blocks; within each block, union of hook commands
120
+ // (compared by `command` string). Never removes user-added entries.
121
+ function mergeSettings(existing, template) {
122
+ const merged = { ...existing };
123
+ for (const [key, value] of Object.entries(template)) {
124
+ if (key === 'hooks') {
125
+ merged.hooks = mergeHooks(existing.hooks || {}, value);
126
+ } else if (!(key in merged)) {
127
+ merged[key] = value;
128
+ }
129
+ }
130
+ return merged;
131
+ }
132
+
133
+ function mergeHooks(existing, template) {
134
+ const merged = { ...existing };
135
+ for (const [event, templateBlocks] of Object.entries(template)) {
136
+ const resultBlocks = [...(merged[event] || [])];
137
+ for (const tplBlock of templateBlocks) {
138
+ const idx = resultBlocks.findIndex(b => b.matcher === tplBlock.matcher);
139
+ if (idx === -1) {
140
+ resultBlocks.push(tplBlock);
141
+ } else {
142
+ const newHooks = [...(resultBlocks[idx].hooks || [])];
143
+ for (const tplHook of tplBlock.hooks || []) {
144
+ if (!newHooks.some(h => h.command === tplHook.command)) {
145
+ newHooks.push(tplHook);
146
+ }
147
+ }
148
+ resultBlocks[idx] = { ...resultBlocks[idx], hooks: newHooks };
149
+ }
150
+ }
151
+ merged[event] = resultBlocks;
152
+ }
153
+ return merged;
154
+ }
155
+
113
156
  // ============================================================================
114
157
  // Step 1: Global Agents (always)
115
158
  // ============================================================================
@@ -243,6 +286,7 @@ async function main() {
243
286
  // ── MCP Servers (interactive selection) ───────────────────────────────
244
287
  let dbType = 'none';
245
288
  let selectedServers = [];
289
+ let adoOrgChoice = null;
246
290
  if (components.includes('mcp')) {
247
291
  console.log(chalk.yellow.bold('\n🔌 MCP Servers → .mcp.json\n'));
248
292
 
@@ -379,6 +423,7 @@ async function main() {
379
423
 
380
424
  dbType = mcpChoices.db;
381
425
  selectedServers = mcpChoices.servers;
426
+ adoOrgChoice = mcpChoices.adoOrg || null;
382
427
 
383
428
  const mcpPath = join(targetDir, '.mcp.json');
384
429
  if (await fs.pathExists(mcpPath)) {
@@ -388,15 +433,16 @@ async function main() {
388
433
  if (newContent.trim() === existingContent.trim()) {
389
434
  console.log(chalk.gray(' = .mcp.json (identical, skipped)'));
390
435
  } else {
436
+ // In --all mode, merge by default — preserves user-added MCP servers.
391
437
  const { action } = installAll
392
- ? { action: 'overwrite' }
438
+ ? { action: 'merge' }
393
439
  : await inquirer.prompt([{
394
440
  type: 'list',
395
441
  name: 'action',
396
442
  message: '.mcp.json already exists:',
397
443
  choices: [
444
+ { name: 'Merge (add missing servers, keep yours)', value: 'merge' },
398
445
  { name: 'Overwrite with new config', value: 'overwrite' },
399
- { name: 'Merge (add missing servers)', value: 'merge' },
400
446
  { name: 'Skip', value: 'skip' },
401
447
  ],
402
448
  }]);
@@ -422,11 +468,27 @@ async function main() {
422
468
  // ── Settings ──────────────────────────────────────────────────────────
423
469
  if (components.includes('settings')) {
424
470
  console.log(chalk.yellow.bold('\n⚙️ Settings → .claude/settings.json\n'));
425
- await installFile(
426
- join(TEMPLATES_DIR, 'infrastructure', 'settings.json'),
427
- join(targetDir, '.claude', 'settings.json'),
428
- 'settings.json'
471
+ const templateSettings = await fs.readJson(
472
+ join(TEMPLATES_DIR, 'infrastructure', 'settings.json')
429
473
  );
474
+ const settingsPath = join(targetDir, '.claude', 'settings.json');
475
+ await fs.ensureDir(dirname(settingsPath));
476
+
477
+ if (!await fs.pathExists(settingsPath)) {
478
+ await fs.writeJson(settingsPath, templateSettings, { spaces: 2 });
479
+ console.log(chalk.green(' ✓ settings.json'));
480
+ } else {
481
+ const existing = await fs.readJson(settingsPath);
482
+ const merged = mergeSettings(existing, templateSettings);
483
+ const before = JSON.stringify(existing);
484
+ const after = JSON.stringify(merged);
485
+ if (before === after) {
486
+ console.log(chalk.gray(' = settings.json (no kit changes needed)'));
487
+ } else {
488
+ await fs.writeJson(settingsPath, merged, { spaces: 2 });
489
+ console.log(chalk.green(' ✓ settings.json (merged — kit hooks added, user customizations preserved)'));
490
+ }
491
+ }
430
492
  }
431
493
 
432
494
  // ── CLAUDE.md Workflow ────────────────────────────────────────────────
@@ -468,18 +530,27 @@ async function main() {
468
530
  existing = await fs.readFile(claudeMdPath, 'utf8');
469
531
 
470
532
  if (existing.includes('Claude Kit Workflow') || existing.includes('Care Solutions AI Workflow')) {
471
- console.log(chalk.gray(' = Workflow section already exists'));
472
- if (!installAll) {
533
+ // Check whether the existing workflow section matches the current template.
534
+ // If it does, nothing to do. If not, replace it (auto in --all, prompt otherwise).
535
+ const cleaned = existing.replace(/\n## (?:Claude Kit Workflow|Care Solutions AI Workflow)[\s\S]*$/, '').trimEnd();
536
+ const expected = `${cleaned}\n\n${workflowContent}`;
537
+ if (existing.trim() === expected.trim()) {
538
+ console.log(chalk.gray(' = Workflow section up to date'));
539
+ } else if (installAll) {
540
+ await fs.writeFile(claudeMdPath, expected);
541
+ console.log(chalk.green(' ✓ Workflow section re-synced to current template'));
542
+ } else {
473
543
  const { replace } = await inquirer.prompt([{
474
544
  type: 'confirm',
475
545
  name: 'replace',
476
- message: 'Replace existing workflow section?',
477
- default: false,
546
+ message: 'Workflow section differs from template — replace?',
547
+ default: true,
478
548
  }]);
479
549
  if (replace) {
480
- const cleaned = existing.replace(/\n## (?:Claude Kit Workflow|Care Solutions AI Workflow)[\s\S]*$/, '').trimEnd();
481
- await fs.writeFile(claudeMdPath, `${cleaned}\n\n${workflowContent}`);
550
+ await fs.writeFile(claudeMdPath, expected);
482
551
  console.log(chalk.green(' ✓ Workflow section replaced'));
552
+ } else {
553
+ console.log(chalk.gray(' ⊘ Workflow section left as-is'));
483
554
  }
484
555
  }
485
556
  } else {
@@ -518,6 +589,19 @@ async function main() {
518
589
  }
519
590
  }
520
591
 
592
+ // ── Install Manifest ──────────────────────────────────────────────────
593
+ // Records the kit version and the choices made (db, adoOrg) so the
594
+ // kit-update-check hook can re-run the installer non-interactively.
595
+ const manifestPath = join(targetDir, '.claude', '.kit-install.json');
596
+ await fs.ensureDir(dirname(manifestPath));
597
+ await fs.writeJson(manifestPath, {
598
+ version: KIT_VERSION,
599
+ choices: {
600
+ db: dbType,
601
+ adoOrg: selectedServers.includes('azuredevops') ? adoOrgChoice : null,
602
+ },
603
+ }, { spaces: 2 });
604
+
521
605
  // ── Summary ───────────────────────────────────────────────────────────
522
606
  const agentCount = (await fs.pathExists(join(targetDir, '.claude', 'agents')))
523
607
  ? (await fs.readdir(join(targetDir, '.claude', 'agents'))).filter(f => f.endsWith('.md')).length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chris1807/claude-kit",
3
- "version": "2.1.16",
3
+ "version": "2.1.18",
4
4
  "description": "Claude Code starter kit for Azure DevOps teams — agents, hooks, MCP servers, slash commands, and end-to-end work item → PR → release → deploy workflow automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -129,11 +129,37 @@ Run a build check **before** any other quality checks. Use the `build-validator`
129
129
 
130
130
  ## Step 7: Quality Checks
131
131
 
132
- 1. **Review** code for quality, security, and Clean Architecture compliance
133
- 2. **Run the full test suite** — every unit test in the repo, plus integration tests. Not just the tests added in this change. A failure in an unrelated test means this change broke something else; treat it as a regression, fix it, and re-run until the entire suite is green
134
- 3. **Run lint** — ESLint and dotnet format
132
+ 1. **Run the full test suite** every unit test in the repo, plus integration tests. Not just the tests added in this change. A failure in an unrelated test means this change broke something else; treat it as a regression, fix it, and re-run until the entire suite is green
133
+ 2. **Run lint** — ESLint and dotnet format
135
134
 
136
- ## Step 8: UAT Gate
135
+ ## Step 8: Code Review
136
+
137
+ Spawn the `reviewer` agent to review the diff for quality, security, Clean Architecture compliance, and CLAUDE.md adherence. The agent is read-only — it reports findings, you act on them.
138
+
139
+ Present the findings to the user grouped by severity:
140
+
141
+ ```
142
+ ## Code Review Findings
143
+
144
+ ### Must-fix (blocking)
145
+ - {file:line} — {issue + why it blocks}
146
+
147
+ ### Should-fix (recommended)
148
+ - {file:line} — {issue + suggested change}
149
+
150
+ ### Nits (optional)
151
+ - {file:line} — {minor note}
152
+
153
+ Address must-fix items? (yes / select / skip)
154
+ ```
155
+
156
+ - `yes` → fix every must-fix item, then re-run the reviewer agent on the updated diff
157
+ - `select` → ask which items to address; fix only those, then re-run the reviewer agent
158
+ - `skip` → proceed without fixes (only allowed if there are no must-fix items, or the user explicitly overrides)
159
+
160
+ Loop until the reviewer reports no must-fix items, or the user explicitly accepts remaining findings. Do not proceed to UAT with unresolved must-fix items unless the user overrides.
161
+
162
+ ## Step 9: UAT Gate
137
163
 
138
164
  ### If Hot Fix:
139
165
  Skip manual UAT. Present an abbreviated confirmation:
@@ -165,7 +191,7 @@ Did manual testing pass?
165
191
 
166
192
  Wait for the user's response before proceeding. Do NOT create a PR until confirmed.
167
193
 
168
- ## Step 9: Push, Create PR, and Update Work Item
194
+ ## Step 10: Push, Create PR, and Update Work Item
169
195
 
170
196
  1. Push the branch: `git push -u origin HEAD`
171
197
  2. Create a PR via Azure DevOps MCP:
@@ -202,10 +202,9 @@ Run a build check **before** any other quality checks. Use the `build-validator`
202
202
 
203
203
  ## Step 9: Quality Checks
204
204
 
205
- 1. **Review** code for quality, security, and Clean Architecture compliance
206
- 2. **Run the full test suite** — every unit test in the repo, plus integration tests. Not just the tests added in this rework. A failure in an unrelated test means this rework broke something else; treat it as a regression, fix it, and re-run until the entire suite is green
207
- 3. **Run lint** — ESLint and dotnet format
208
- 4. **Acceptance Criteria check** — re-read the work item's full Acceptance Criteria (the same list captured in Step 3). For each AC, identify the test or piece of code that proves it's met. If any AC has no covering test or visible code path, flag it before moving to the UAT gate:
205
+ 1. **Run the full test suite** every unit test in the repo, plus integration tests. Not just the tests added in this rework. A failure in an unrelated test means this rework broke something else; treat it as a regression, fix it, and re-run until the entire suite is green
206
+ 2. **Run lint** — ESLint and dotnet format
207
+ 3. **Acceptance Criteria check** — re-read the work item's full Acceptance Criteria (the same list captured in Step 3). For each AC, identify the test or piece of code that proves it's met. If any AC has no covering test or visible code path, flag it before moving on:
209
208
 
210
209
  ```
211
210
  ⚠ AC #{n} ({short form}) has no covering test or clear code path.
@@ -214,7 +213,34 @@ Run a build check **before** any other quality checks. Use the `build-validator`
214
213
 
215
214
  Do not advance to Step 10 with any AC unverified.
216
215
 
217
- ## Step 10: UAT Gate
216
+ ## Step 10: Code Review
217
+
218
+ Spawn the `reviewer` agent to review the rework diff for quality, security, Clean Architecture compliance, and CLAUDE.md adherence. Focus the review on the files changed since the last PR — call out any regression risk introduced by the rework. The agent is read-only — it reports findings, you act on them.
219
+
220
+ Present the findings to the user grouped by severity:
221
+
222
+ ```
223
+ ## Code Review Findings
224
+
225
+ ### Must-fix (blocking)
226
+ - {file:line} — {issue + why it blocks}
227
+
228
+ ### Should-fix (recommended)
229
+ - {file:line} — {issue + suggested change}
230
+
231
+ ### Nits (optional)
232
+ - {file:line} — {minor note}
233
+
234
+ Address must-fix items? (yes / select / skip)
235
+ ```
236
+
237
+ - `yes` → fix every must-fix item, then re-run the reviewer agent on the updated diff
238
+ - `select` → ask which items to address; fix only those, then re-run the reviewer agent
239
+ - `skip` → proceed without fixes (only allowed if there are no must-fix items, or the user explicitly overrides)
240
+
241
+ Loop until the reviewer reports no must-fix items, or the user explicitly accepts remaining findings. Do not proceed to UAT with unresolved must-fix items unless the user overrides.
242
+
243
+ ## Step 11: UAT Gate
218
244
 
219
245
  ### If Hot Fix:
220
246
  Skip manual UAT. Present an abbreviated confirmation:
@@ -246,7 +272,7 @@ Did manual testing pass?
246
272
 
247
273
  Wait for the user's response before proceeding. Do NOT push until confirmed.
248
274
 
249
- ## Step 11: Push and Update
275
+ ## Step 12: Push and Update
250
276
 
251
277
  1. Push the changes: `git push`
252
278
  2. Add a comment on the existing PR summarizing what was changed in the rework
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # kit-update-check.sh — SessionStart hook
3
+ #
4
+ # Checks npm for a newer @chris1807/claude-kit and re-runs the installer
5
+ # non-interactively when one is available. Must never block claude startup —
6
+ # every external call is timeout-bounded and every failure exits 0 silently.
7
+ #
8
+ # Throttled to once per 24h via a marker file. Reads .claude/.kit-install.json
9
+ # for the installed version and the choices to pass to the re-install (db, adoOrg).
10
+
11
+ set +e # never propagate failures
12
+
13
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ CLAUDE_DIR="$(cd "$HOOK_DIR/.." && pwd)"
15
+ PROJECT_DIR="$(cd "$CLAUDE_DIR/.." && pwd)"
16
+ MANIFEST="$CLAUDE_DIR/.kit-install.json"
17
+ MARKER="$CLAUDE_DIR/.kit-update-check"
18
+ LOG="$CLAUDE_DIR/.kit-update.log"
19
+
20
+ # No manifest = legacy install or global-only; nothing to do.
21
+ [ -f "$MANIFEST" ] || exit 0
22
+
23
+ # Throttle to one check per 24h. A failed check still touches the marker so
24
+ # unreachable npm doesn't retry every session.
25
+ if [ -f "$MARKER" ]; then
26
+ if command -v stat >/dev/null 2>&1; then
27
+ LAST=$(stat -f %m "$MARKER" 2>/dev/null || stat -c %Y "$MARKER" 2>/dev/null || echo 0)
28
+ NOW=$(date +%s)
29
+ if [ "$((NOW - LAST))" -lt 86400 ]; then
30
+ exit 0
31
+ fi
32
+ fi
33
+ fi
34
+
35
+ # Crude JSON extraction (avoids requiring jq).
36
+ extract() {
37
+ grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$MANIFEST" | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
38
+ }
39
+ INSTALLED=$(extract version)
40
+ DB=$(extract db)
41
+ ADO_ORG=$(extract adoOrg)
42
+
43
+ # Fetch latest with a 5s timeout — must never hang. `timeout` isn't on macOS
44
+ # by default; perl's alarm() is the most portable fallback.
45
+ if command -v timeout >/dev/null 2>&1; then
46
+ LATEST=$(timeout 5 npm view @chris1807/claude-kit version 2>/dev/null)
47
+ elif command -v gtimeout >/dev/null 2>&1; then
48
+ LATEST=$(gtimeout 5 npm view @chris1807/claude-kit version 2>/dev/null)
49
+ elif command -v perl >/dev/null 2>&1; then
50
+ LATEST=$(perl -e 'alarm 5; exec @ARGV or exit 1' npm view @chris1807/claude-kit version 2>/dev/null)
51
+ else
52
+ # No timeout mechanism available — skip rather than risk a hang.
53
+ touch "$MARKER"
54
+ exit 0
55
+ fi
56
+
57
+ touch "$MARKER"
58
+
59
+ [ -z "$LATEST" ] && exit 0
60
+ [ -z "$INSTALLED" ] && exit 0
61
+ [ "$LATEST" = "$INSTALLED" ] && exit 0
62
+
63
+ # Only proceed if LATEST sorts strictly greater than INSTALLED.
64
+ NEWER=$(printf '%s\n%s\n' "$INSTALLED" "$LATEST" | sort -V | tail -1)
65
+ [ "$NEWER" = "$LATEST" ] || exit 0
66
+ [ "$NEWER" = "$INSTALLED" ] && exit 0
67
+
68
+ echo "claude-kit: updating $INSTALLED → $LATEST..." >&2
69
+
70
+ ARGS=("@chris1807/claude-kit" "init" "$PROJECT_DIR" "--all")
71
+ if [ -n "$DB" ] && [ "$DB" != "null" ]; then
72
+ ARGS+=("--db=$DB")
73
+ fi
74
+ if [ -n "$ADO_ORG" ] && [ "$ADO_ORG" != "null" ]; then
75
+ ARGS+=("--ado-org=$ADO_ORG")
76
+ fi
77
+
78
+ if npx -y "${ARGS[@]}" >"$LOG" 2>&1; then
79
+ echo "claude-kit: updated to $LATEST (log: $LOG)" >&2
80
+ else
81
+ echo "claude-kit: update failed (see $LOG)" >&2
82
+ fi
83
+
84
+ exit 0
@@ -89,6 +89,16 @@
89
89
  }
90
90
  ]
91
91
  }
92
+ ],
93
+ "SessionStart": [
94
+ {
95
+ "hooks": [
96
+ {
97
+ "type": "command",
98
+ "command": "bash .claude/hooks/kit-update-check.sh"
99
+ }
100
+ ]
101
+ }
92
102
  ]
93
103
  }
94
104
  }