harness-evolve 1.0.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/dist/cli.js ADDED
@@ -0,0 +1,1685 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "@commander-js/extra-typings";
5
+ import { readFileSync } from "fs";
6
+ import { join as join10 } from "path";
7
+
8
+ // src/cli/init.ts
9
+ import { copyFile, mkdir, access as access2, writeFile } from "fs/promises";
10
+ import { dirname as dirname3, join as join3 } from "path";
11
+
12
+ // src/cli/utils.ts
13
+ import { readFile } from "fs/promises";
14
+ import { join } from "path";
15
+ import { createInterface } from "readline/promises";
16
+ import writeFileAtomic from "write-file-atomic";
17
+ var HARNESS_EVOLVE_MARKER = "harness-evolve";
18
+ var SETTINGS_PATH = join(
19
+ process.env.HOME ?? "",
20
+ ".claude",
21
+ "settings.json"
22
+ );
23
+ var HOOK_REGISTRATIONS = [
24
+ {
25
+ event: "UserPromptSubmit",
26
+ hookFile: "user-prompt-submit.js",
27
+ timeout: 10,
28
+ async: false,
29
+ description: "Captures prompts and delivers optimization notifications"
30
+ },
31
+ {
32
+ event: "PreToolUse",
33
+ hookFile: "pre-tool-use.js",
34
+ timeout: 10,
35
+ async: true,
36
+ description: "Tracks tool usage patterns before execution"
37
+ },
38
+ {
39
+ event: "PostToolUse",
40
+ hookFile: "post-tool-use.js",
41
+ timeout: 10,
42
+ async: true,
43
+ description: "Records tool outcomes for pattern analysis"
44
+ },
45
+ {
46
+ event: "PostToolUseFailure",
47
+ hookFile: "post-tool-use-failure.js",
48
+ timeout: 10,
49
+ async: true,
50
+ description: "Logs tool failures to detect correction patterns"
51
+ },
52
+ {
53
+ event: "PermissionRequest",
54
+ hookFile: "permission-request.js",
55
+ timeout: 10,
56
+ async: true,
57
+ description: "Monitors permission decisions for auto-approval suggestions"
58
+ },
59
+ {
60
+ event: "Stop",
61
+ hookFile: "stop.js",
62
+ timeout: 10,
63
+ async: true,
64
+ description: "Triggers analysis when interaction threshold is reached"
65
+ }
66
+ ];
67
+ function resolveHookPath(hookFile, baseDirOverride) {
68
+ const baseDir = baseDirOverride ?? import.meta.dirname;
69
+ return join(baseDir, "hooks", hookFile);
70
+ }
71
+ async function readSettings(settingsPath) {
72
+ const filePath = settingsPath ?? SETTINGS_PATH;
73
+ try {
74
+ const raw = await readFile(filePath, "utf-8");
75
+ return JSON.parse(raw);
76
+ } catch {
77
+ return {};
78
+ }
79
+ }
80
+ async function writeSettings(settings, settingsPath) {
81
+ const filePath = settingsPath ?? SETTINGS_PATH;
82
+ await writeFileAtomic(filePath, JSON.stringify(settings, null, 2));
83
+ }
84
+ function mergeHooks(existing, hookCommands) {
85
+ const hooks = existing.hooks != null ? { ...existing.hooks } : {};
86
+ for (const hc of hookCommands) {
87
+ const eventArray = Array.isArray(hooks[hc.event]) ? [...hooks[hc.event]] : [];
88
+ const alreadyRegistered = eventArray.some((entry) => {
89
+ const innerHooks = entry.hooks;
90
+ if (!Array.isArray(innerHooks)) return false;
91
+ return innerHooks.some(
92
+ (h) => String(h.command ?? "").includes(HARNESS_EVOLVE_MARKER)
93
+ );
94
+ });
95
+ if (!alreadyRegistered) {
96
+ const hookEntry = {
97
+ type: "command",
98
+ command: hc.command,
99
+ timeout: hc.timeout
100
+ };
101
+ if (hc.async) {
102
+ hookEntry.async = true;
103
+ }
104
+ eventArray.push({
105
+ matcher: "*",
106
+ hooks: [hookEntry]
107
+ });
108
+ }
109
+ hooks[hc.event] = eventArray;
110
+ }
111
+ return { ...existing, hooks };
112
+ }
113
+ async function confirm(message) {
114
+ const rl = createInterface({
115
+ input: process.stdin,
116
+ output: process.stdout
117
+ });
118
+ try {
119
+ const answer = await rl.question(`${message} [y/N] `);
120
+ return /^y(es)?$/i.test(answer.trim());
121
+ } finally {
122
+ rl.close();
123
+ }
124
+ }
125
+
126
+ // src/scan/context-builder.ts
127
+ import { readFile as readFile2, readdir } from "fs/promises";
128
+ import { join as join2, basename } from "path";
129
+
130
+ // src/scan/schemas.ts
131
+ import { z } from "zod/v4";
132
+ var scanContextSchema = z.object({
133
+ generated_at: z.iso.datetime(),
134
+ project_root: z.string(),
135
+ claude_md_files: z.array(
136
+ z.object({
137
+ path: z.string(),
138
+ scope: z.enum(["user", "project", "local"]),
139
+ content: z.string(),
140
+ line_count: z.number(),
141
+ headings: z.array(z.string()),
142
+ references: z.array(z.string())
143
+ })
144
+ ),
145
+ rules: z.array(
146
+ z.object({
147
+ path: z.string(),
148
+ filename: z.string(),
149
+ content: z.string(),
150
+ frontmatter: z.object({
151
+ paths: z.array(z.string()).optional()
152
+ }).optional(),
153
+ headings: z.array(z.string())
154
+ })
155
+ ),
156
+ settings: z.object({
157
+ user: z.unknown().nullable(),
158
+ project: z.unknown().nullable(),
159
+ local: z.unknown().nullable()
160
+ }),
161
+ commands: z.array(
162
+ z.object({
163
+ path: z.string(),
164
+ name: z.string(),
165
+ content: z.string()
166
+ })
167
+ ),
168
+ hooks_registered: z.array(
169
+ z.object({
170
+ event: z.string(),
171
+ scope: z.enum(["user", "project", "local"]),
172
+ type: z.string(),
173
+ command: z.string().optional()
174
+ })
175
+ )
176
+ });
177
+
178
+ // src/scan/context-builder.ts
179
+ async function readFileSafe(path) {
180
+ try {
181
+ return await readFile2(path, "utf-8");
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ function extractHeadings(content) {
187
+ const headings = [];
188
+ const regex = /^#{1,6}\s+(.+)$/gm;
189
+ let match;
190
+ while ((match = regex.exec(content)) !== null) {
191
+ headings.push(match[1].trim());
192
+ }
193
+ return headings;
194
+ }
195
+ function extractReferences(content) {
196
+ const refs = [];
197
+ const regex = /@([\w./-]+)/g;
198
+ let match;
199
+ while ((match = regex.exec(content)) !== null) {
200
+ const ref = match[1];
201
+ const idx = match.index;
202
+ if (idx > 0 && /\w/.test(content[idx - 1])) {
203
+ continue;
204
+ }
205
+ const cleaned = ref.replace(/\.$/, "");
206
+ refs.push(cleaned);
207
+ }
208
+ return refs;
209
+ }
210
+ async function readClaudeMdFiles(cwd, home) {
211
+ const locations = [
212
+ { path: join2(cwd, "CLAUDE.md"), scope: "project" },
213
+ { path: join2(cwd, ".claude", "CLAUDE.md"), scope: "local" },
214
+ { path: join2(home, ".claude", "CLAUDE.md"), scope: "user" }
215
+ ];
216
+ const files = [];
217
+ for (const loc of locations) {
218
+ const content = await readFileSafe(loc.path);
219
+ if (content !== null) {
220
+ files.push({
221
+ path: loc.path,
222
+ scope: loc.scope,
223
+ content,
224
+ line_count: content.split("\n").length,
225
+ headings: extractHeadings(content),
226
+ references: extractReferences(content)
227
+ });
228
+ }
229
+ }
230
+ return files;
231
+ }
232
+ async function collectMdFiles(dir) {
233
+ const results = [];
234
+ try {
235
+ const entries = await readdir(dir, { withFileTypes: true });
236
+ for (const entry of entries) {
237
+ const fullPath = join2(dir, entry.name);
238
+ if (entry.isDirectory()) {
239
+ const nested = await collectMdFiles(fullPath);
240
+ results.push(...nested);
241
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
242
+ results.push(fullPath);
243
+ }
244
+ }
245
+ } catch {
246
+ }
247
+ return results;
248
+ }
249
+ function parseFrontmatter(content) {
250
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
251
+ if (!match) return void 0;
252
+ const frontmatter = match[1];
253
+ const pathsMatch = frontmatter.match(
254
+ /paths:\s*\n((?:\s*-\s*.+\n?)*)/
255
+ );
256
+ if (!pathsMatch) return {};
257
+ const paths2 = pathsMatch[1].split("\n").map((line) => line.replace(/^\s*-\s*/, "").trim()).filter((line) => line.length > 0);
258
+ return paths2.length > 0 ? { paths: paths2 } : {};
259
+ }
260
+ async function readRuleFiles(cwd) {
261
+ const rulesDir = join2(cwd, ".claude", "rules");
262
+ const mdFiles = await collectMdFiles(rulesDir);
263
+ const rules = [];
264
+ for (const filePath of mdFiles) {
265
+ const content = await readFileSafe(filePath);
266
+ if (content !== null) {
267
+ rules.push({
268
+ path: filePath,
269
+ filename: basename(filePath),
270
+ content,
271
+ frontmatter: parseFrontmatter(content),
272
+ headings: extractHeadings(content)
273
+ });
274
+ }
275
+ }
276
+ return rules;
277
+ }
278
+ async function readAllSettings(cwd, home) {
279
+ const settingsPaths = {
280
+ user: join2(home, ".claude", "settings.json"),
281
+ project: join2(cwd, ".claude", "settings.json"),
282
+ local: join2(cwd, ".claude", "settings.local.json")
283
+ };
284
+ const readJsonSafe = async (path) => {
285
+ try {
286
+ const raw = await readFile2(path, "utf-8");
287
+ return JSON.parse(raw);
288
+ } catch {
289
+ return null;
290
+ }
291
+ };
292
+ const [user, project, local] = await Promise.all([
293
+ readJsonSafe(settingsPaths.user),
294
+ readJsonSafe(settingsPaths.project),
295
+ readJsonSafe(settingsPaths.local)
296
+ ]);
297
+ return { user, project, local };
298
+ }
299
+ async function readCommandFiles(cwd) {
300
+ const commandsDir = join2(cwd, ".claude", "commands");
301
+ const commands = [];
302
+ try {
303
+ const entries = await readdir(commandsDir, { withFileTypes: true });
304
+ for (const entry of entries) {
305
+ if (entry.isFile() && entry.name.endsWith(".md")) {
306
+ const filePath = join2(commandsDir, entry.name);
307
+ const content = await readFileSafe(filePath);
308
+ if (content !== null) {
309
+ commands.push({
310
+ path: filePath,
311
+ name: entry.name.replace(/\.md$/, ""),
312
+ content
313
+ });
314
+ }
315
+ }
316
+ }
317
+ } catch {
318
+ }
319
+ return commands;
320
+ }
321
+ function extractHooksFromAllSettings(settings) {
322
+ const hooks = [];
323
+ const extractFromScope = (settingsObj, scope) => {
324
+ if (!settingsObj || typeof settingsObj !== "object") return;
325
+ const obj = settingsObj;
326
+ if (!obj.hooks || typeof obj.hooks !== "object") return;
327
+ const hooksConfig = obj.hooks;
328
+ for (const [event, defs] of Object.entries(hooksConfig)) {
329
+ if (!Array.isArray(defs)) continue;
330
+ for (const def of defs) {
331
+ if (!def || typeof def !== "object") continue;
332
+ const hookDef = def;
333
+ const type = String(hookDef.type ?? "command");
334
+ const command = typeof hookDef.command === "string" ? hookDef.command : void 0;
335
+ hooks.push({ event, scope, type, command });
336
+ }
337
+ }
338
+ };
339
+ extractFromScope(settings.user, "user");
340
+ extractFromScope(settings.project, "project");
341
+ extractFromScope(settings.local, "local");
342
+ return hooks;
343
+ }
344
+ async function buildScanContext(cwd, home) {
345
+ const homeDir = home ?? process.env.HOME ?? "";
346
+ const [claudeMdFiles, rules, settings, commands] = await Promise.all([
347
+ readClaudeMdFiles(cwd, homeDir),
348
+ readRuleFiles(cwd),
349
+ readAllSettings(cwd, homeDir),
350
+ readCommandFiles(cwd)
351
+ ]);
352
+ const hooksRegistered = extractHooksFromAllSettings(settings);
353
+ const ctx = {
354
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
355
+ project_root: cwd,
356
+ claude_md_files: claudeMdFiles,
357
+ rules,
358
+ settings,
359
+ commands,
360
+ hooks_registered: hooksRegistered
361
+ };
362
+ return scanContextSchema.parse(ctx);
363
+ }
364
+
365
+ // src/scan/scanners/redundancy.ts
366
+ function normalizeText(text) {
367
+ return text.toLowerCase().trim().replace(/\s+/g, " ");
368
+ }
369
+ function scanRedundancy(context) {
370
+ const recommendations = [];
371
+ let index = 0;
372
+ const claudeMdHeadings = context.claude_md_files.flatMap(
373
+ (f) => f.headings.map((h) => ({ heading: normalizeText(h), source: f.path }))
374
+ );
375
+ const ruleHeadings = context.rules.flatMap(
376
+ (r) => r.headings.map((h) => ({ heading: normalizeText(h), source: r.path }))
377
+ );
378
+ for (const cmdH of claudeMdHeadings) {
379
+ const match = ruleHeadings.find((rH) => rH.heading === cmdH.heading);
380
+ if (match) {
381
+ recommendations.push({
382
+ id: `rec-scan-redundancy-${index++}`,
383
+ target: "RULE",
384
+ confidence: "MEDIUM",
385
+ pattern_type: "scan_redundancy",
386
+ title: `Redundant section: "${cmdH.heading}"`,
387
+ description: `The heading "${cmdH.heading}" appears in both ${cmdH.source} and ${match.source}. This may indicate duplicated instructions.`,
388
+ evidence: {
389
+ count: 2,
390
+ examples: [cmdH.source, match.source]
391
+ },
392
+ suggested_action: "Consolidate into one location. If it belongs in rules, remove from CLAUDE.md. If it belongs in CLAUDE.md, remove the rule file."
393
+ });
394
+ }
395
+ }
396
+ const rulesByHeadingSet = /* @__PURE__ */ new Map();
397
+ for (const rule of context.rules) {
398
+ const key = rule.headings.map((h) => normalizeText(h)).sort().join("||");
399
+ if (!key) continue;
400
+ const existing = rulesByHeadingSet.get(key) ?? [];
401
+ existing.push(rule.path);
402
+ rulesByHeadingSet.set(key, existing);
403
+ }
404
+ for (const [, paths2] of rulesByHeadingSet) {
405
+ if (paths2.length < 2) continue;
406
+ recommendations.push({
407
+ id: `rec-scan-redundancy-${index++}`,
408
+ target: "RULE",
409
+ confidence: "MEDIUM",
410
+ pattern_type: "scan_redundancy",
411
+ title: `Duplicate rule files detected (${paths2.length} files with same headings)`,
412
+ description: `${paths2.length} rule files share the same heading structure: ${paths2.join(", ")}. They may contain redundant content.`,
413
+ evidence: {
414
+ count: paths2.length,
415
+ examples: paths2.slice(0, 3)
416
+ },
417
+ suggested_action: "Review these rule files and merge them into a single file, or differentiate their content."
418
+ });
419
+ }
420
+ return recommendations;
421
+ }
422
+
423
+ // src/scan/scanners/mechanization.ts
424
+ var MECHANIZATION_INDICATORS = [
425
+ { regex: /always\s+run\s+["`']?(\S+)/i, hookEvent: "PreToolUse", label: "always run" },
426
+ {
427
+ regex: /before\s+committing?,?\s+run\s+["`']?(\S+)/i,
428
+ hookEvent: "PreToolUse",
429
+ label: "pre-commit check"
430
+ },
431
+ {
432
+ regex: /after\s+every\s+(?:edit|change|write)/i,
433
+ hookEvent: "PostToolUse",
434
+ label: "post-edit action"
435
+ },
436
+ {
437
+ regex: /must\s+(?:always\s+)?check\s+["`']?(\S+)/i,
438
+ hookEvent: "PreToolUse",
439
+ label: "mandatory check"
440
+ },
441
+ {
442
+ regex: /never\s+(?:allow|permit|run)\s+["`']?(\S+)/i,
443
+ hookEvent: "PreToolUse",
444
+ label: "forbidden operation"
445
+ },
446
+ {
447
+ regex: /forbidden.*(?:rm\s+-rf|drop\s+|delete\s+|truncate)/i,
448
+ hookEvent: "PreToolUse",
449
+ label: "dangerous command guard"
450
+ }
451
+ ];
452
+ function scanMechanization(context) {
453
+ const recommendations = [];
454
+ let index = 0;
455
+ const allTextSources = [
456
+ ...context.claude_md_files.map((f) => ({ content: f.content, source: f.path })),
457
+ ...context.rules.map((r) => ({ content: r.content, source: r.path }))
458
+ ];
459
+ for (const source of allTextSources) {
460
+ for (const indicator of MECHANIZATION_INDICATORS) {
461
+ const match = source.content.match(indicator.regex);
462
+ if (!match) continue;
463
+ const alreadyCovered = context.hooks_registered.some(
464
+ (h) => h.event === indicator.hookEvent
465
+ );
466
+ if (alreadyCovered) continue;
467
+ recommendations.push({
468
+ id: `rec-scan-mechanize-${index++}`,
469
+ target: "HOOK",
470
+ confidence: "MEDIUM",
471
+ pattern_type: "scan_missing_mechanization",
472
+ title: `Mechanizable rule: "${match[0].substring(0, 60)}"`,
473
+ description: `Found a rule in ${source.source} that describes an operation suitable for a ${indicator.hookEvent} hook: "${match[0]}". Hooks provide 100% reliable execution, while rules depend on Claude's probabilistic compliance.`,
474
+ evidence: {
475
+ count: 1,
476
+ examples: [match[0].substring(0, 100)]
477
+ },
478
+ suggested_action: `Create a ${indicator.hookEvent} hook to enforce this rule automatically. See Claude Code hooks docs for ${indicator.hookEvent} event.`
479
+ });
480
+ }
481
+ }
482
+ return recommendations;
483
+ }
484
+
485
+ // src/scan/scanners/staleness.ts
486
+ import { access } from "fs/promises";
487
+ import { constants } from "fs";
488
+ import { resolve, dirname as dirname2 } from "path";
489
+ async function fileExistsOnDisk(filePath) {
490
+ try {
491
+ await access(filePath, constants.F_OK);
492
+ return true;
493
+ } catch {
494
+ return false;
495
+ }
496
+ }
497
+ function extractPathFromCommand(command) {
498
+ const quotedMatch = command.match(/(?:node|sh|bash|python)\s+["']([^"']+)["']/i);
499
+ if (quotedMatch) return quotedMatch[1];
500
+ const unquotedMatch = command.match(
501
+ /(?:node|sh|bash|python)\s+(\S+\.(?:js|ts|sh|py|mjs|cjs))/i
502
+ );
503
+ if (unquotedMatch) return unquotedMatch[1];
504
+ return null;
505
+ }
506
+ async function scanStaleness(context) {
507
+ const recommendations = [];
508
+ let index = 0;
509
+ for (const claudeMd of context.claude_md_files) {
510
+ for (const ref of claudeMd.references) {
511
+ const resolved = resolve(dirname2(claudeMd.path), ref);
512
+ const inContext = context.rules.some((r) => r.path === resolved) || context.claude_md_files.some((f) => f.path === resolved) || context.commands.some((c) => c.path === resolved);
513
+ if (inContext) continue;
514
+ const existsOnDisk = await fileExistsOnDisk(resolved);
515
+ if (existsOnDisk) continue;
516
+ recommendations.push({
517
+ id: `rec-scan-stale-${index++}`,
518
+ target: "CLAUDE_MD",
519
+ confidence: "HIGH",
520
+ pattern_type: "scan_stale_reference",
521
+ title: `Stale reference: @${ref}`,
522
+ description: `${claudeMd.path} references @${ref}, but this file does not exist.`,
523
+ evidence: {
524
+ count: 1,
525
+ examples: [`@${ref} in ${claudeMd.path}`]
526
+ },
527
+ suggested_action: `Remove the @${ref} reference from ${claudeMd.path}, or create the missing file.`
528
+ });
529
+ }
530
+ }
531
+ for (const hook of context.hooks_registered) {
532
+ if (!hook.command) continue;
533
+ const scriptPath = extractPathFromCommand(hook.command);
534
+ if (!scriptPath) continue;
535
+ const exists = await fileExistsOnDisk(scriptPath);
536
+ if (exists) continue;
537
+ recommendations.push({
538
+ id: `rec-scan-stale-${index++}`,
539
+ target: "SETTINGS",
540
+ confidence: "HIGH",
541
+ pattern_type: "scan_stale_reference",
542
+ title: `Stale hook script: ${scriptPath}`,
543
+ description: `Hook (${hook.event}, ${hook.scope}) references script "${scriptPath}", but this file does not exist. The hook will fail when triggered.`,
544
+ evidence: {
545
+ count: 1,
546
+ examples: [`${hook.event} hook command: ${hook.command}`]
547
+ },
548
+ suggested_action: `Create the missing script at "${scriptPath}", or update the hook command in settings.json.`
549
+ });
550
+ }
551
+ return recommendations;
552
+ }
553
+
554
+ // src/scan/scanners/index.ts
555
+ var scanners = [scanRedundancy, scanMechanization, scanStaleness];
556
+
557
+ // src/scan/index.ts
558
+ async function runDeepScan(cwd, home) {
559
+ const scanContext = await buildScanContext(cwd, home);
560
+ const recommendations = [];
561
+ for (const scanner of scanners) {
562
+ try {
563
+ const result = await scanner(scanContext);
564
+ recommendations.push(...result);
565
+ } catch (err) {
566
+ console.error(
567
+ `Scanner error: ${err instanceof Error ? err.message : String(err)}`
568
+ );
569
+ }
570
+ }
571
+ return {
572
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
573
+ scan_context: scanContext,
574
+ recommendations
575
+ };
576
+ }
577
+
578
+ // src/commands/evolve-scan.ts
579
+ function generateScanCommand() {
580
+ return `---
581
+ name: scan
582
+ description: Run a deep harness-evolve configuration scan to detect quality issues
583
+ disable-model-invocation: true
584
+ ---
585
+
586
+ # Evolve Scan
587
+
588
+ Run a deep scan of the current project's Claude Code configuration to detect quality issues and optimization opportunities.
589
+
590
+ ## What This Does
591
+
592
+ Analyzes your Claude Code configuration files to detect:
593
+ - **Redundant rules** -- same constraint defined in multiple files (CLAUDE.md, .claude/rules/, settings.json)
594
+ - **Missing mechanization** -- operations in rules or CLAUDE.md that should be hooks for 100% reliability
595
+ - **Stale config** -- references to non-existent files, outdated commands, or unused settings
596
+ - **Configuration drift** -- inconsistencies between .claude/commands/, rules, and settings
597
+
598
+ Files scanned: CLAUDE.md, .claude/rules/, settings.json, .claude/commands/
599
+
600
+ ## Instructions
601
+
602
+ Run the scan CLI command:
603
+
604
+ \`\`\`bash
605
+ npx harness-evolve scan
606
+ \`\`\`
607
+
608
+ Or if globally installed:
609
+
610
+ \`\`\`bash
611
+ harness-evolve scan
612
+ \`\`\`
613
+
614
+ ## Presenting Results
615
+
616
+ Present the results grouped by confidence level (HIGH first, then MEDIUM, then LOW):
617
+
618
+ 1. **HIGH confidence** -- Issues that are very likely real problems. Recommend immediate action.
619
+ 2. **MEDIUM confidence** -- Probable issues worth reviewing. Present with context for user to decide.
620
+ 3. **LOW confidence** -- Possible improvements. Mention briefly and let user prioritize.
621
+
622
+ For each issue, show:
623
+ - Confidence level and category
624
+ - Description of the problem
625
+ - Affected file(s)
626
+ - Suggested fix
627
+
628
+ If issues are found, suggest running \`/evolve:apply\` to review and apply the recommendations interactively.
629
+
630
+ If no issues are found, congratulate the user on a clean configuration.
631
+ `;
632
+ }
633
+
634
+ // src/commands/evolve-apply.ts
635
+ function generateApplyCommand() {
636
+ return `---
637
+ name: apply
638
+ description: Review and apply pending harness-evolve recommendations one by one
639
+ disable-model-invocation: true
640
+ argument-hint: "[filter: all|high|medium|low]"
641
+ ---
642
+
643
+ # Evolve Apply
644
+
645
+ Review pending harness-evolve recommendations interactively. For each recommendation, you can choose to apply it, skip it for later, or permanently dismiss it.
646
+
647
+ ## Arguments
648
+
649
+ If \`$ARGUMENTS\` is provided (e.g., "high", "medium", or "low"), filter recommendations to only show that confidence level. If empty or "all", show all pending recommendations.
650
+
651
+ ## Instructions
652
+
653
+ ### Step 1: Read Pending Recommendations
654
+
655
+ \`\`\`bash
656
+ npx harness-evolve pending
657
+ \`\`\`
658
+
659
+ If the command outputs no pending recommendations, inform the user:
660
+ > No pending recommendations. Run \`/evolve:scan\` to analyze your configuration and generate new recommendations.
661
+
662
+ ### Step 2: Present Each Recommendation
663
+
664
+ For each pending recommendation, present the following:
665
+
666
+ - **Confidence level** (HIGH / MEDIUM / LOW)
667
+ - **Title** -- what the recommendation is about
668
+ - **Description** -- detailed explanation of the issue
669
+ - **Evidence** -- what data or pattern triggered this recommendation
670
+ - **Suggested action** -- what change is proposed
671
+
672
+ ### Step 3: Ask User to Choose
673
+
674
+ For each recommendation, ask the user to choose one of three actions:
675
+
676
+ 1. **Apply** -- Execute the recommendation. Run:
677
+ \`\`\`bash
678
+ npx harness-evolve apply-one <id>
679
+ \`\`\`
680
+ Report the result (success or failure) and show what changed.
681
+
682
+ 2. **Skip** -- Do nothing for now. The recommendation stays pending for future review. No command needed.
683
+
684
+ 3. **Ignore** -- Permanently dismiss this recommendation. Run:
685
+ \`\`\`bash
686
+ npx harness-evolve dismiss <id>
687
+ \`\`\`
688
+ Confirm the recommendation has been dismissed.
689
+
690
+ ### Step 4: Continue or Finish
691
+
692
+ After processing each recommendation, move to the next one. When all recommendations have been processed, summarize what was done:
693
+ - How many applied
694
+ - How many skipped
695
+ - How many ignored
696
+
697
+ ## Notes
698
+
699
+ - Recommendations are generated by \`harness-evolve scan\` or automatic background analysis
700
+ - Applied recommendations modify configuration files (settings.json, CLAUDE.md, .claude/rules/, etc.)
701
+ - Ignored recommendations will not appear in future \`/evolve:apply\` sessions
702
+ - Skipped recommendations remain pending and will reappear next time
703
+ `;
704
+ }
705
+
706
+ // src/cli/init.ts
707
+ async function fileExists(path) {
708
+ try {
709
+ await access2(path);
710
+ return true;
711
+ } catch {
712
+ return false;
713
+ }
714
+ }
715
+ async function installSlashCommands(projectDir) {
716
+ const commandsDir = join3(projectDir, ".claude", "commands", "evolve");
717
+ await mkdir(commandsDir, { recursive: true });
718
+ const commands = [
719
+ { name: "scan", generate: generateScanCommand, path: join3(commandsDir, "scan.md") },
720
+ { name: "apply", generate: generateApplyCommand, path: join3(commandsDir, "apply.md") }
721
+ ];
722
+ for (const cmd of commands) {
723
+ if (await fileExists(cmd.path)) {
724
+ console.log(` /evolve:${cmd.name} already installed, skipping`);
725
+ } else {
726
+ await writeFile(cmd.path, cmd.generate(), "utf-8");
727
+ console.log(` /evolve:${cmd.name} installed`);
728
+ }
729
+ }
730
+ }
731
+ async function runInit(options) {
732
+ const settingsPath = options.settingsPath ?? SETTINGS_PATH;
733
+ const hookCommands = HOOK_REGISTRATIONS.map((reg) => {
734
+ const absolutePath = resolveHookPath(reg.hookFile, options.baseDirOverride);
735
+ return {
736
+ event: reg.event,
737
+ command: `node "${absolutePath}"`,
738
+ timeout: reg.timeout,
739
+ async: reg.async,
740
+ description: reg.description
741
+ };
742
+ });
743
+ console.log("\nPlanned hook registrations:\n");
744
+ for (const hc of hookCommands) {
745
+ const asyncLabel = hc.async ? " (async)" : "";
746
+ console.log(` ${hc.event}${asyncLabel} -- ${hc.description}`);
747
+ console.log(` -> ${hc.command}`);
748
+ }
749
+ console.log("");
750
+ const samplePath = hookCommands[0].command;
751
+ if (samplePath.includes(".npm/_npx/")) {
752
+ console.log(
753
+ "WARNING: Detected npx installation. Hook paths may break when the"
754
+ );
755
+ console.log(
756
+ "npx cache is cleared. For persistent installation, use: npm i -g harness-evolve\n"
757
+ );
758
+ }
759
+ if (!options.yes) {
760
+ const accepted = await confirm("Register hooks in settings.json?");
761
+ if (!accepted) {
762
+ console.log("Aborted.");
763
+ return;
764
+ }
765
+ }
766
+ await mkdir(dirname3(settingsPath), { recursive: true });
767
+ const exists = await fileExists(settingsPath);
768
+ if (exists) {
769
+ await copyFile(settingsPath, settingsPath + ".backup");
770
+ console.log(`Backup created: ${settingsPath}.backup`);
771
+ }
772
+ const settings = await readSettings(settingsPath);
773
+ const merged = mergeHooks(settings, hookCommands);
774
+ await writeSettings(merged, settingsPath);
775
+ console.log(
776
+ `Hooks registered successfully! (${hookCommands.length} events)`
777
+ );
778
+ console.log("\nInstalling slash commands...\n");
779
+ await installSlashCommands(options.projectDir ?? process.cwd());
780
+ try {
781
+ console.log("\nScanning configuration...\n");
782
+ const scanResult = await runDeepScan(process.cwd());
783
+ if (scanResult.recommendations.length > 0) {
784
+ console.log(
785
+ `Found ${scanResult.recommendations.length} configuration suggestion(s):
786
+ `
787
+ );
788
+ for (const rec of scanResult.recommendations) {
789
+ console.log(` [${rec.confidence}] ${rec.title}`);
790
+ console.log(` ${rec.description}`);
791
+ console.log(` Suggested: ${rec.suggested_action}
792
+ `);
793
+ }
794
+ } else {
795
+ console.log("Configuration looks clean -- no issues detected.\n");
796
+ }
797
+ } catch (err) {
798
+ console.error(
799
+ `Warning: Configuration scan failed: ${err instanceof Error ? err.message : String(err)}`
800
+ );
801
+ }
802
+ }
803
+ function registerInitCommand(program2) {
804
+ program2.command("init").description("Register harness-evolve hooks in Claude Code settings").option("--yes", "Skip confirmation prompt").action(async (opts) => {
805
+ await runInit({ yes: opts.yes ?? false });
806
+ });
807
+ }
808
+
809
+ // src/storage/counter.ts
810
+ import { readFile as readFile3 } from "fs/promises";
811
+ import { lock } from "proper-lockfile";
812
+ import writeFileAtomic2 from "write-file-atomic";
813
+
814
+ // src/schemas/counter.ts
815
+ import { z as z2 } from "zod/v4";
816
+ var counterSchema = z2.object({
817
+ total: z2.number().default(0),
818
+ session: z2.record(z2.string(), z2.number()).default({}),
819
+ last_analysis: z2.iso.datetime().optional(),
820
+ last_updated: z2.iso.datetime()
821
+ });
822
+
823
+ // src/storage/dirs.ts
824
+ import { mkdir as mkdir2 } from "fs/promises";
825
+ import { join as join4 } from "path";
826
+ var BASE_DIR = join4(process.env.HOME ?? "", ".harness-evolve");
827
+ var paths = {
828
+ base: BASE_DIR,
829
+ logs: {
830
+ prompts: join4(BASE_DIR, "logs", "prompts"),
831
+ tools: join4(BASE_DIR, "logs", "tools"),
832
+ permissions: join4(BASE_DIR, "logs", "permissions"),
833
+ sessions: join4(BASE_DIR, "logs", "sessions")
834
+ },
835
+ analysis: join4(BASE_DIR, "analysis"),
836
+ analysisPreProcessed: join4(BASE_DIR, "analysis", "pre-processed"),
837
+ summary: join4(BASE_DIR, "analysis", "pre-processed", "summary.json"),
838
+ environmentSnapshot: join4(BASE_DIR, "analysis", "environment-snapshot.json"),
839
+ analysisResult: join4(BASE_DIR, "analysis", "analysis-result.json"),
840
+ pending: join4(BASE_DIR, "pending"),
841
+ config: join4(BASE_DIR, "config.json"),
842
+ counter: join4(BASE_DIR, "counter.json"),
843
+ recommendations: join4(BASE_DIR, "recommendations.md"),
844
+ recommendationState: join4(BASE_DIR, "analysis", "recommendation-state.json"),
845
+ recommendationArchive: join4(BASE_DIR, "analysis", "recommendations-archive"),
846
+ notificationFlag: join4(BASE_DIR, "analysis", "has-pending-notifications"),
847
+ autoApplyLog: join4(BASE_DIR, "analysis", "auto-apply-log.jsonl"),
848
+ outcomeHistory: join4(BASE_DIR, "analysis", "outcome-history.jsonl")
849
+ };
850
+ var initialized = false;
851
+ async function ensureInit() {
852
+ if (initialized) return;
853
+ await mkdir2(paths.logs.prompts, { recursive: true });
854
+ await mkdir2(paths.logs.tools, { recursive: true });
855
+ await mkdir2(paths.logs.permissions, { recursive: true });
856
+ await mkdir2(paths.logs.sessions, { recursive: true });
857
+ await mkdir2(paths.analysis, { recursive: true });
858
+ await mkdir2(paths.analysisPreProcessed, { recursive: true });
859
+ await mkdir2(paths.pending, { recursive: true });
860
+ await mkdir2(paths.recommendationArchive, { recursive: true });
861
+ initialized = true;
862
+ }
863
+
864
+ // src/storage/counter.ts
865
+ async function readCounter() {
866
+ await ensureInit();
867
+ try {
868
+ const raw = await readFile3(paths.counter, "utf-8");
869
+ return counterSchema.parse(JSON.parse(raw));
870
+ } catch {
871
+ return {
872
+ total: 0,
873
+ session: {},
874
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
875
+ };
876
+ }
877
+ }
878
+
879
+ // src/delivery/state.ts
880
+ import { readFile as readFile4 } from "fs/promises";
881
+ import writeFileAtomic3 from "write-file-atomic";
882
+
883
+ // src/schemas/delivery.ts
884
+ import { z as z3 } from "zod/v4";
885
+ var recommendationStatusSchema = z3.enum(["pending", "applied", "dismissed"]);
886
+ var recommendationStateEntrySchema = z3.object({
887
+ id: z3.string(),
888
+ status: recommendationStatusSchema,
889
+ updated_at: z3.iso.datetime(),
890
+ applied_details: z3.string().optional()
891
+ });
892
+ var recommendationStateSchema = z3.object({
893
+ entries: z3.array(recommendationStateEntrySchema),
894
+ last_updated: z3.iso.datetime()
895
+ });
896
+ var autoApplyLogEntrySchema = z3.object({
897
+ timestamp: z3.iso.datetime(),
898
+ recommendation_id: z3.string(),
899
+ target: z3.string(),
900
+ action: z3.string(),
901
+ success: z3.boolean(),
902
+ details: z3.string().optional(),
903
+ backup_path: z3.string().optional()
904
+ });
905
+
906
+ // src/delivery/state.ts
907
+ async function loadState() {
908
+ try {
909
+ const raw = await readFile4(paths.recommendationState, "utf-8");
910
+ return recommendationStateSchema.parse(JSON.parse(raw));
911
+ } catch (err) {
912
+ if (isNodeError(err) && err.code === "ENOENT") {
913
+ return { entries: [], last_updated: (/* @__PURE__ */ new Date()).toISOString() };
914
+ }
915
+ throw err;
916
+ }
917
+ }
918
+ async function saveState(state) {
919
+ await writeFileAtomic3(
920
+ paths.recommendationState,
921
+ JSON.stringify(state, null, 2)
922
+ );
923
+ }
924
+ async function updateStatus(id, status, details) {
925
+ const state = await loadState();
926
+ const now = (/* @__PURE__ */ new Date()).toISOString();
927
+ const existing = state.entries.find((e) => e.id === id);
928
+ if (existing) {
929
+ existing.status = status;
930
+ existing.updated_at = now;
931
+ if (status === "applied" && details !== void 0) {
932
+ existing.applied_details = details;
933
+ } else if (status !== "applied") {
934
+ existing.applied_details = void 0;
935
+ }
936
+ } else {
937
+ state.entries.push({
938
+ id,
939
+ status,
940
+ updated_at: now,
941
+ ...status === "applied" && details !== void 0 ? { applied_details: details } : {}
942
+ });
943
+ }
944
+ state.last_updated = now;
945
+ await saveState(state);
946
+ }
947
+ function isNodeError(err) {
948
+ return err instanceof Error && "code" in err;
949
+ }
950
+
951
+ // src/cli/status.ts
952
+ async function runStatus(options) {
953
+ const settingsPath = options.settingsPath ?? SETTINGS_PATH;
954
+ const counter = await readCounter();
955
+ const state = await loadState();
956
+ const pendingCount = state.entries.filter(
957
+ (e) => e.status === "pending"
958
+ ).length;
959
+ const settings = await readSettings(settingsPath);
960
+ const hooksRegistered = JSON.stringify(settings.hooks ?? {}).includes(
961
+ HARNESS_EVOLVE_MARKER
962
+ );
963
+ console.log("");
964
+ console.log("harness-evolve status");
965
+ console.log("=====================");
966
+ console.log(`Interactions: ${counter.total}`);
967
+ console.log(`Last analysis: ${counter.last_analysis ?? "never"}`);
968
+ console.log(`Pending recs: ${pendingCount}`);
969
+ console.log(`Hooks registered: ${hooksRegistered ? "Yes" : "No"}`);
970
+ console.log("");
971
+ }
972
+ function registerStatusCommand(program2) {
973
+ program2.command("status").description("Show harness-evolve status and statistics").action(async () => {
974
+ await runStatus({});
975
+ });
976
+ }
977
+
978
+ // src/cli/uninstall.ts
979
+ import { copyFile as copyFile2, rm, rmdir, access as access3 } from "fs/promises";
980
+ import { constants as constants2 } from "fs";
981
+ import { join as join5 } from "path";
982
+ async function removeSlashCommands(projectDir) {
983
+ const commandsDir = join5(projectDir, ".claude", "commands", "evolve");
984
+ for (const file of ["scan.md", "apply.md"]) {
985
+ try {
986
+ await rm(join5(commandsDir, file));
987
+ console.log(` Removed /evolve:${file.replace(".md", "")}`);
988
+ } catch {
989
+ }
990
+ }
991
+ try {
992
+ await rmdir(commandsDir);
993
+ } catch {
994
+ }
995
+ }
996
+ async function runUninstall(options) {
997
+ const settingsPath = options.settingsPath ?? SETTINGS_PATH;
998
+ const settings = await readSettings(settingsPath);
999
+ const hooks = settings.hooks;
1000
+ if (!hooks || Object.keys(hooks).length === 0) {
1001
+ console.log("No harness-evolve hooks found in settings.json");
1002
+ } else {
1003
+ let removedCount = 0;
1004
+ const filteredHooks = {};
1005
+ for (const [event, entries] of Object.entries(hooks)) {
1006
+ if (!Array.isArray(entries)) {
1007
+ filteredHooks[event] = entries;
1008
+ continue;
1009
+ }
1010
+ const kept = entries.filter((entry) => {
1011
+ const innerHooks = entry.hooks;
1012
+ if (!Array.isArray(innerHooks)) return true;
1013
+ const isHarnessEvolve = innerHooks.some(
1014
+ (h) => String(h.command ?? "").includes(HARNESS_EVOLVE_MARKER)
1015
+ );
1016
+ if (isHarnessEvolve) removedCount++;
1017
+ return !isHarnessEvolve;
1018
+ });
1019
+ if (kept.length > 0) {
1020
+ filteredHooks[event] = kept;
1021
+ }
1022
+ }
1023
+ if (removedCount > 0) {
1024
+ await copyFile2(settingsPath, settingsPath + ".backup");
1025
+ console.log(`Backup created: ${settingsPath}.backup`);
1026
+ settings.hooks = filteredHooks;
1027
+ await writeSettings(settings, settingsPath);
1028
+ console.log("Removed harness-evolve hooks from settings.json");
1029
+ } else {
1030
+ console.log("No harness-evolve hooks found in settings.json");
1031
+ }
1032
+ }
1033
+ console.log("\nRemoving slash commands...");
1034
+ await removeSlashCommands(options.projectDir ?? process.cwd());
1035
+ if (options.purge) {
1036
+ if (!options.yes) {
1037
+ const accepted = await confirm(
1038
+ "Delete all harness-evolve data (~/.harness-evolve/)?"
1039
+ );
1040
+ if (!accepted) {
1041
+ console.log("Data directory preserved.");
1042
+ return;
1043
+ }
1044
+ }
1045
+ try {
1046
+ await access3(paths.base, constants2.F_OK);
1047
+ await rm(paths.base, { recursive: true, force: true });
1048
+ console.log(`Deleted data directory: ${paths.base}`);
1049
+ } catch {
1050
+ console.log(`Data directory not found: ${paths.base}`);
1051
+ }
1052
+ }
1053
+ }
1054
+ function registerUninstallCommand(program2) {
1055
+ program2.command("uninstall").description("Remove harness-evolve hooks and optionally delete data").option("--purge", "Also delete ~/.harness-evolve/ data directory").option("--yes", "Skip confirmation prompt").action(async (opts) => {
1056
+ await runUninstall({
1057
+ purge: opts.purge ?? false,
1058
+ yes: opts.yes ?? false
1059
+ });
1060
+ });
1061
+ }
1062
+
1063
+ // src/cli/scan.ts
1064
+ var CONFIDENCE_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2 };
1065
+ function registerScanCommand(program2) {
1066
+ program2.command("scan").description("Run deep configuration scan and output results as JSON").action(async () => {
1067
+ try {
1068
+ const result = await runDeepScan(process.cwd());
1069
+ const sorted = [...result.recommendations].sort(
1070
+ (a, b) => (CONFIDENCE_ORDER[a.confidence] ?? 3) - (CONFIDENCE_ORDER[b.confidence] ?? 3)
1071
+ );
1072
+ const output = {
1073
+ generated_at: result.generated_at,
1074
+ recommendation_count: sorted.length,
1075
+ recommendations: sorted
1076
+ };
1077
+ console.log(JSON.stringify(output, null, 2));
1078
+ } catch (err) {
1079
+ console.log(JSON.stringify({
1080
+ error: err instanceof Error ? err.message : String(err),
1081
+ recommendations: []
1082
+ }, null, 2));
1083
+ process.exitCode = 1;
1084
+ }
1085
+ });
1086
+ }
1087
+
1088
+ // src/cli/apply.ts
1089
+ import { readFile as readFile8, appendFile as appendFile2 } from "fs/promises";
1090
+
1091
+ // src/schemas/recommendation.ts
1092
+ import { z as z4 } from "zod/v4";
1093
+ var routingTargetSchema = z4.enum([
1094
+ "HOOK",
1095
+ "SKILL",
1096
+ "RULE",
1097
+ "CLAUDE_MD",
1098
+ "MEMORY",
1099
+ "SETTINGS"
1100
+ ]);
1101
+ var confidenceSchema = z4.enum(["HIGH", "MEDIUM", "LOW"]);
1102
+ var patternTypeSchema = z4.enum([
1103
+ "repeated_prompt",
1104
+ "long_prompt",
1105
+ "permission-always-approved",
1106
+ "code_correction",
1107
+ "personal_info",
1108
+ "config_drift",
1109
+ "version_update",
1110
+ "ecosystem_gsd",
1111
+ "ecosystem_cog",
1112
+ "onboarding_start_hooks",
1113
+ "onboarding_start_rules",
1114
+ "onboarding_start_claudemd",
1115
+ "onboarding_optimize",
1116
+ "scan_redundancy",
1117
+ "scan_missing_mechanization",
1118
+ "scan_stale_reference"
1119
+ ]);
1120
+ var recommendationSchema = z4.object({
1121
+ id: z4.string(),
1122
+ target: routingTargetSchema,
1123
+ confidence: confidenceSchema,
1124
+ pattern_type: patternTypeSchema,
1125
+ title: z4.string(),
1126
+ description: z4.string(),
1127
+ evidence: z4.object({
1128
+ count: z4.number(),
1129
+ sessions: z4.number().optional(),
1130
+ examples: z4.array(z4.string()).max(3)
1131
+ }),
1132
+ suggested_action: z4.string(),
1133
+ ecosystem_context: z4.string().optional()
1134
+ });
1135
+ var DEFAULT_THRESHOLDS = {
1136
+ repeated_prompt_min_count: 5,
1137
+ repeated_prompt_high_count: 10,
1138
+ repeated_prompt_high_sessions: 3,
1139
+ repeated_prompt_medium_sessions: 2,
1140
+ long_prompt_min_words: 200,
1141
+ long_prompt_min_count: 2,
1142
+ long_prompt_high_words: 300,
1143
+ long_prompt_high_count: 3,
1144
+ permission_approval_min_count: 10,
1145
+ permission_approval_min_sessions: 3,
1146
+ permission_approval_high_count: 15,
1147
+ permission_approval_high_sessions: 4,
1148
+ code_correction_min_failure_rate: 0.3,
1149
+ code_correction_min_failures: 3
1150
+ };
1151
+ var analysisConfigSchema = z4.object({
1152
+ thresholds: z4.object({
1153
+ repeated_prompt_min_count: z4.number().default(DEFAULT_THRESHOLDS.repeated_prompt_min_count),
1154
+ repeated_prompt_high_count: z4.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_count),
1155
+ repeated_prompt_high_sessions: z4.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_sessions),
1156
+ repeated_prompt_medium_sessions: z4.number().default(DEFAULT_THRESHOLDS.repeated_prompt_medium_sessions),
1157
+ long_prompt_min_words: z4.number().default(DEFAULT_THRESHOLDS.long_prompt_min_words),
1158
+ long_prompt_min_count: z4.number().default(DEFAULT_THRESHOLDS.long_prompt_min_count),
1159
+ long_prompt_high_words: z4.number().default(DEFAULT_THRESHOLDS.long_prompt_high_words),
1160
+ long_prompt_high_count: z4.number().default(DEFAULT_THRESHOLDS.long_prompt_high_count),
1161
+ permission_approval_min_count: z4.number().default(DEFAULT_THRESHOLDS.permission_approval_min_count),
1162
+ permission_approval_min_sessions: z4.number().default(DEFAULT_THRESHOLDS.permission_approval_min_sessions),
1163
+ permission_approval_high_count: z4.number().default(DEFAULT_THRESHOLDS.permission_approval_high_count),
1164
+ permission_approval_high_sessions: z4.number().default(DEFAULT_THRESHOLDS.permission_approval_high_sessions),
1165
+ code_correction_min_failure_rate: z4.number().default(DEFAULT_THRESHOLDS.code_correction_min_failure_rate),
1166
+ code_correction_min_failures: z4.number().default(DEFAULT_THRESHOLDS.code_correction_min_failures)
1167
+ }).default(() => ({ ...DEFAULT_THRESHOLDS })),
1168
+ max_recommendations: z4.number().default(20)
1169
+ }).default(() => ({
1170
+ thresholds: { ...DEFAULT_THRESHOLDS },
1171
+ max_recommendations: 20
1172
+ }));
1173
+ var analysisResultSchema = z4.object({
1174
+ generated_at: z4.iso.datetime(),
1175
+ summary_period: z4.object({
1176
+ since: z4.string(),
1177
+ until: z4.string(),
1178
+ days: z4.number()
1179
+ }),
1180
+ recommendations: z4.array(recommendationSchema),
1181
+ metadata: z4.object({
1182
+ classifier_count: z4.number(),
1183
+ patterns_evaluated: z4.number(),
1184
+ environment_ecosystems: z4.array(z4.string()),
1185
+ claude_code_version: z4.string()
1186
+ })
1187
+ });
1188
+
1189
+ // src/delivery/auto-apply.ts
1190
+ import { appendFile } from "fs/promises";
1191
+
1192
+ // src/storage/config.ts
1193
+ import { readFile as readFile5 } from "fs/promises";
1194
+ import writeFileAtomic4 from "write-file-atomic";
1195
+
1196
+ // src/schemas/config.ts
1197
+ import { z as z5 } from "zod/v4";
1198
+ var configSchema = z5.object({
1199
+ version: z5.number().default(1),
1200
+ analysis: z5.object({
1201
+ threshold: z5.number().min(1).default(50),
1202
+ enabled: z5.boolean().default(true),
1203
+ classifierThresholds: z5.record(z5.string(), z5.number()).default({})
1204
+ }).default({ threshold: 50, enabled: true, classifierThresholds: {} }),
1205
+ hooks: z5.object({
1206
+ capturePrompts: z5.boolean().default(true),
1207
+ captureTools: z5.boolean().default(true),
1208
+ capturePermissions: z5.boolean().default(true),
1209
+ captureSessions: z5.boolean().default(true)
1210
+ }).default({
1211
+ capturePrompts: true,
1212
+ captureTools: true,
1213
+ capturePermissions: true,
1214
+ captureSessions: true
1215
+ }),
1216
+ scrubbing: z5.object({
1217
+ enabled: z5.boolean().default(true),
1218
+ highEntropyDetection: z5.boolean().default(false),
1219
+ customPatterns: z5.array(z5.object({
1220
+ name: z5.string(),
1221
+ regex: z5.string(),
1222
+ replacement: z5.string()
1223
+ })).default([])
1224
+ }).default({
1225
+ enabled: true,
1226
+ highEntropyDetection: false,
1227
+ customPatterns: []
1228
+ }),
1229
+ delivery: z5.object({
1230
+ stdoutInjection: z5.boolean().default(true),
1231
+ maxTokens: z5.number().default(200),
1232
+ fullAuto: z5.boolean().default(false),
1233
+ maxRecommendationsInFile: z5.number().default(20),
1234
+ archiveAfterDays: z5.number().default(7)
1235
+ }).default({
1236
+ stdoutInjection: true,
1237
+ maxTokens: 200,
1238
+ fullAuto: false,
1239
+ maxRecommendationsInFile: 20,
1240
+ archiveAfterDays: 7
1241
+ })
1242
+ }).strict();
1243
+
1244
+ // src/delivery/appliers/index.ts
1245
+ var registry = /* @__PURE__ */ new Map();
1246
+ function registerApplier(applier) {
1247
+ registry.set(applier.target, applier);
1248
+ }
1249
+ function getApplier(target) {
1250
+ return registry.get(target);
1251
+ }
1252
+
1253
+ // src/delivery/appliers/settings-applier.ts
1254
+ import { readFile as readFile6, copyFile as copyFile3, mkdir as mkdir3 } from "fs/promises";
1255
+ import { join as join6, dirname as dirname4 } from "path";
1256
+ import writeFileAtomic5 from "write-file-atomic";
1257
+ var SettingsApplier = class {
1258
+ target = "SETTINGS";
1259
+ canApply(rec) {
1260
+ return rec.confidence === "HIGH" && rec.target === "SETTINGS" && rec.pattern_type === "permission-always-approved";
1261
+ }
1262
+ async apply(rec, options) {
1263
+ try {
1264
+ if (rec.pattern_type !== "permission-always-approved") {
1265
+ return {
1266
+ recommendation_id: rec.id,
1267
+ success: false,
1268
+ details: `Skipped: pattern_type '${rec.pattern_type}' not supported for auto-apply in v1`
1269
+ };
1270
+ }
1271
+ const settingsFilePath = options?.settingsPath ?? join6(process.env.HOME ?? "", ".claude", "settings.json");
1272
+ const raw = await readFile6(settingsFilePath, "utf-8");
1273
+ const settings = JSON.parse(raw);
1274
+ const backup = join6(
1275
+ paths.analysis,
1276
+ "backups",
1277
+ `settings-backup-${rec.id}.json`
1278
+ );
1279
+ await mkdir3(dirname4(backup), { recursive: true });
1280
+ await copyFile3(settingsFilePath, backup);
1281
+ const toolName = extractToolName(rec);
1282
+ if (!toolName) {
1283
+ return {
1284
+ recommendation_id: rec.id,
1285
+ success: false,
1286
+ details: "Could not extract tool name from recommendation evidence"
1287
+ };
1288
+ }
1289
+ const allowedTools = Array.isArray(settings.allowedTools) ? settings.allowedTools : [];
1290
+ if (!allowedTools.includes(toolName)) {
1291
+ allowedTools.push(toolName);
1292
+ }
1293
+ settings.allowedTools = allowedTools;
1294
+ await writeFileAtomic5(
1295
+ settingsFilePath,
1296
+ JSON.stringify(settings, null, 2)
1297
+ );
1298
+ return {
1299
+ recommendation_id: rec.id,
1300
+ success: true,
1301
+ details: `Added ${toolName} to allowedTools`
1302
+ };
1303
+ } catch (err) {
1304
+ const message = err instanceof Error ? err.message : String(err);
1305
+ return {
1306
+ recommendation_id: rec.id,
1307
+ success: false,
1308
+ details: message
1309
+ };
1310
+ }
1311
+ }
1312
+ };
1313
+ function extractToolName(rec) {
1314
+ for (const example of rec.evidence.examples) {
1315
+ const match = example.match(/^(\w+)\(/);
1316
+ if (match) return match[1];
1317
+ }
1318
+ return void 0;
1319
+ }
1320
+
1321
+ // src/delivery/appliers/rule-applier.ts
1322
+ import { writeFile as writeFile2, access as access4, mkdir as mkdir4 } from "fs/promises";
1323
+ import { join as join7 } from "path";
1324
+ var RuleApplier = class {
1325
+ target = "RULE";
1326
+ canApply(rec) {
1327
+ return rec.confidence === "HIGH" && rec.target === "RULE";
1328
+ }
1329
+ async apply(rec, options) {
1330
+ const rulesDir = options?.rulesDir ?? join7(process.env.HOME ?? "", ".claude", "rules");
1331
+ const fileName = `evolve-${rec.pattern_type}.md`;
1332
+ const filePath = join7(rulesDir, fileName);
1333
+ try {
1334
+ await access4(filePath);
1335
+ return {
1336
+ recommendation_id: rec.id,
1337
+ success: false,
1338
+ details: `Rule file already exists: ${fileName}`
1339
+ };
1340
+ } catch {
1341
+ }
1342
+ try {
1343
+ await mkdir4(rulesDir, { recursive: true });
1344
+ const content = [
1345
+ `# ${rec.title}`,
1346
+ "",
1347
+ rec.description,
1348
+ "",
1349
+ "## Action",
1350
+ "",
1351
+ rec.suggested_action,
1352
+ "",
1353
+ "---",
1354
+ `*Auto-generated by harness-evolve (${rec.id})*`
1355
+ ].join("\n");
1356
+ await writeFile2(filePath, content, "utf-8");
1357
+ return {
1358
+ recommendation_id: rec.id,
1359
+ success: true,
1360
+ details: `Created rule file: ${fileName}`
1361
+ };
1362
+ } catch (err) {
1363
+ const message = err instanceof Error ? err.message : String(err);
1364
+ return {
1365
+ recommendation_id: rec.id,
1366
+ success: false,
1367
+ details: message
1368
+ };
1369
+ }
1370
+ }
1371
+ };
1372
+
1373
+ // src/delivery/appliers/hook-applier.ts
1374
+ import { writeFile as writeFile3, access as access5, mkdir as mkdir5, chmod, copyFile as copyFile4 } from "fs/promises";
1375
+ import { join as join8, basename as basename2 } from "path";
1376
+
1377
+ // src/generators/schemas.ts
1378
+ import { z as z6 } from "zod/v4";
1379
+ var GENERATOR_VERSION = "1.0.0";
1380
+ function nowISO() {
1381
+ return (/* @__PURE__ */ new Date()).toISOString();
1382
+ }
1383
+ function toSlug(text) {
1384
+ if (!text) return "";
1385
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1386
+ }
1387
+ var generatedArtifactSchema = z6.object({
1388
+ type: z6.enum(["skill", "hook", "claude_md_patch"]),
1389
+ filename: z6.string(),
1390
+ content: z6.string(),
1391
+ source_recommendation_id: z6.string(),
1392
+ metadata: z6.object({
1393
+ generated_at: z6.iso.datetime(),
1394
+ generator_version: z6.string(),
1395
+ pattern_type: z6.string()
1396
+ })
1397
+ });
1398
+
1399
+ // src/generators/hook-generator.ts
1400
+ function extractHookEvent(rec) {
1401
+ const descMatch = rec.description.match(/suitable for a (\w+) hook/i);
1402
+ if (descMatch) return descMatch[1];
1403
+ const actionMatch = rec.suggested_action.match(/Create a (\w+) hook/i);
1404
+ if (actionMatch) return actionMatch[1];
1405
+ return "PreToolUse";
1406
+ }
1407
+ function generateHook(rec) {
1408
+ if (rec.target !== "HOOK") return null;
1409
+ const hookEvent = extractHookEvent(rec);
1410
+ const slugName = toSlug(rec.title);
1411
+ const content = [
1412
+ "#!/usr/bin/env bash",
1413
+ `# Auto-generated hook for: ${rec.title}`,
1414
+ `# Hook event: ${hookEvent}`,
1415
+ `# Source: harness-evolve (${rec.id})`,
1416
+ "#",
1417
+ "# TODO: Review and customize this script before use.",
1418
+ "",
1419
+ "# Read hook input from stdin",
1420
+ "INPUT=$(cat)",
1421
+ "",
1422
+ "# Extract relevant fields",
1423
+ `# Adjust jq path based on your ${hookEvent} event schema`,
1424
+ "",
1425
+ `# ${rec.suggested_action}`,
1426
+ "",
1427
+ "# Exit 0 to allow, exit 2 to block",
1428
+ "exit 0"
1429
+ ].join("\n");
1430
+ return {
1431
+ type: "hook",
1432
+ filename: `.claude/hooks/evolve-${slugName}.sh`,
1433
+ content,
1434
+ source_recommendation_id: rec.id,
1435
+ metadata: {
1436
+ generated_at: nowISO(),
1437
+ generator_version: GENERATOR_VERSION,
1438
+ pattern_type: rec.pattern_type
1439
+ }
1440
+ };
1441
+ }
1442
+
1443
+ // src/delivery/appliers/hook-applier.ts
1444
+ var HookApplier = class {
1445
+ target = "HOOK";
1446
+ canApply(rec) {
1447
+ return rec.confidence === "HIGH" && rec.target === "HOOK";
1448
+ }
1449
+ async apply(rec, options) {
1450
+ try {
1451
+ const artifact = generateHook(rec);
1452
+ if (!artifact) {
1453
+ return {
1454
+ recommendation_id: rec.id,
1455
+ success: false,
1456
+ details: "Generator returned null \u2014 recommendation not applicable for hook generation"
1457
+ };
1458
+ }
1459
+ const hooksDir = options?.hooksDir ?? join8(process.env.HOME ?? "", ".claude", "hooks");
1460
+ const scriptFilename = basename2(artifact.filename);
1461
+ const scriptPath = join8(hooksDir, scriptFilename);
1462
+ try {
1463
+ await access5(scriptPath);
1464
+ return {
1465
+ recommendation_id: rec.id,
1466
+ success: false,
1467
+ details: `Hook file already exists: ${scriptFilename}`
1468
+ };
1469
+ } catch {
1470
+ }
1471
+ await mkdir5(hooksDir, { recursive: true });
1472
+ await writeFile3(scriptPath, artifact.content, "utf-8");
1473
+ await chmod(scriptPath, 493);
1474
+ const settingsPath = options?.settingsPath ?? join8(process.env.HOME ?? "", ".claude", "settings.json");
1475
+ const settings = await readSettings(settingsPath);
1476
+ const backupDir = join8(paths.analysis, "backups");
1477
+ await mkdir5(backupDir, { recursive: true });
1478
+ const backupFile = join8(backupDir, `settings-backup-${rec.id}.json`);
1479
+ try {
1480
+ await copyFile4(settingsPath, backupFile);
1481
+ } catch {
1482
+ await writeFile3(backupFile, JSON.stringify(settings, null, 2), "utf-8");
1483
+ }
1484
+ const eventMatch = artifact.content.match(/# Hook event: (\w+)/);
1485
+ const hookEvent = eventMatch?.[1] ?? "PreToolUse";
1486
+ const merged = mergeHooks(settings, [
1487
+ {
1488
+ event: hookEvent,
1489
+ command: `bash "${scriptPath}"`,
1490
+ timeout: 10,
1491
+ async: true
1492
+ }
1493
+ ]);
1494
+ await writeSettings(merged, settingsPath);
1495
+ return {
1496
+ recommendation_id: rec.id,
1497
+ success: true,
1498
+ details: `Created hook script: ${scriptFilename} and registered under ${hookEvent}`
1499
+ };
1500
+ } catch (err) {
1501
+ const message = err instanceof Error ? err.message : String(err);
1502
+ return {
1503
+ recommendation_id: rec.id,
1504
+ success: false,
1505
+ details: message
1506
+ };
1507
+ }
1508
+ }
1509
+ };
1510
+
1511
+ // src/delivery/appliers/claude-md-applier.ts
1512
+ import { readFile as readFile7, mkdir as mkdir6 } from "fs/promises";
1513
+ import { join as join9, dirname as dirname6 } from "path";
1514
+ import writeFileAtomic6 from "write-file-atomic";
1515
+ var DESTRUCTIVE_PATTERNS = /* @__PURE__ */ new Set([
1516
+ "scan_stale_reference",
1517
+ "scan_redundancy"
1518
+ ]);
1519
+ var ClaudeMdApplier = class {
1520
+ target = "CLAUDE_MD";
1521
+ canApply(rec) {
1522
+ return rec.confidence === "HIGH" && rec.target === "CLAUDE_MD";
1523
+ }
1524
+ async apply(rec, options) {
1525
+ try {
1526
+ if (DESTRUCTIVE_PATTERNS.has(rec.pattern_type)) {
1527
+ return {
1528
+ recommendation_id: rec.id,
1529
+ success: false,
1530
+ details: `Pattern type '${rec.pattern_type}' requires manual review \u2014 cannot safely auto-apply`
1531
+ };
1532
+ }
1533
+ const claudeMdPath = options?.claudeMdPath ?? join9(process.cwd(), "CLAUDE.md");
1534
+ let existingContent = "";
1535
+ try {
1536
+ existingContent = await readFile7(claudeMdPath, "utf-8");
1537
+ } catch {
1538
+ }
1539
+ if (existingContent) {
1540
+ const backupDir = join9(paths.analysis, "backups");
1541
+ await mkdir6(backupDir, { recursive: true });
1542
+ const backupFile = join9(backupDir, `claudemd-backup-${rec.id}.md`);
1543
+ await writeFileAtomic6(backupFile, existingContent);
1544
+ }
1545
+ const newSection = [
1546
+ "",
1547
+ "",
1548
+ `## ${rec.title}`,
1549
+ "",
1550
+ rec.suggested_action,
1551
+ "",
1552
+ "---",
1553
+ `*Auto-generated by harness-evolve (${rec.id})*`,
1554
+ ""
1555
+ ].join("\n");
1556
+ const updatedContent = existingContent + newSection;
1557
+ await mkdir6(dirname6(claudeMdPath), { recursive: true });
1558
+ await writeFileAtomic6(claudeMdPath, updatedContent);
1559
+ return {
1560
+ recommendation_id: rec.id,
1561
+ success: true,
1562
+ details: `Appended section: ${rec.title}`
1563
+ };
1564
+ } catch (err) {
1565
+ const message = err instanceof Error ? err.message : String(err);
1566
+ return {
1567
+ recommendation_id: rec.id,
1568
+ success: false,
1569
+ details: message
1570
+ };
1571
+ }
1572
+ }
1573
+ };
1574
+
1575
+ // src/delivery/auto-apply.ts
1576
+ registerApplier(new SettingsApplier());
1577
+ registerApplier(new RuleApplier());
1578
+ registerApplier(new HookApplier());
1579
+ registerApplier(new ClaudeMdApplier());
1580
+
1581
+ // src/cli/apply.ts
1582
+ var CONFIDENCE_ORDER2 = { HIGH: 0, MEDIUM: 1, LOW: 2 };
1583
+ async function loadAnalysisResult() {
1584
+ try {
1585
+ const raw = await readFile8(paths.analysisResult, "utf-8");
1586
+ const parsed = analysisResultSchema.parse(JSON.parse(raw));
1587
+ return parsed.recommendations;
1588
+ } catch {
1589
+ return [];
1590
+ }
1591
+ }
1592
+ function registerPendingCommand(program2) {
1593
+ program2.command("pending").description("List pending recommendations as JSON").action(async () => {
1594
+ const allRecs = await loadAnalysisResult();
1595
+ const state = await loadState();
1596
+ const statusMap = new Map(state.entries.map((e) => [e.id, e.status]));
1597
+ const pending = allRecs.filter((rec) => {
1598
+ const status = statusMap.get(rec.id);
1599
+ return status === void 0 || status === "pending";
1600
+ }).sort(
1601
+ (a, b) => (CONFIDENCE_ORDER2[a.confidence] ?? 3) - (CONFIDENCE_ORDER2[b.confidence] ?? 3)
1602
+ );
1603
+ console.log(JSON.stringify({ pending, count: pending.length }, null, 2));
1604
+ });
1605
+ }
1606
+ function registerApplyOneCommand(program2) {
1607
+ program2.command("apply-one").description("Apply a single recommendation by ID").argument("<id>", "Recommendation ID to apply").action(async (id) => {
1608
+ try {
1609
+ const allRecs = await loadAnalysisResult();
1610
+ const rec = allRecs.find((r) => r.id === id);
1611
+ if (!rec) {
1612
+ console.log(JSON.stringify({
1613
+ recommendation_id: id,
1614
+ success: false,
1615
+ details: `Recommendation '${id}' not found`
1616
+ }));
1617
+ process.exitCode = 1;
1618
+ return;
1619
+ }
1620
+ const applier = getApplier(rec.target);
1621
+ if (!applier || !applier.canApply(rec)) {
1622
+ console.log(JSON.stringify({
1623
+ recommendation_id: id,
1624
+ success: false,
1625
+ details: `No applicable applier for target '${rec.target}'`
1626
+ }));
1627
+ process.exitCode = 1;
1628
+ return;
1629
+ }
1630
+ const result = await applier.apply(rec);
1631
+ const logEntry = {
1632
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1633
+ recommendation_id: rec.id,
1634
+ target: rec.target,
1635
+ action: rec.suggested_action,
1636
+ success: result.success,
1637
+ details: result.details
1638
+ };
1639
+ try {
1640
+ await appendFile2(paths.autoApplyLog, JSON.stringify(logEntry) + "\n", "utf-8");
1641
+ } catch {
1642
+ }
1643
+ if (result.success) {
1644
+ await updateStatus(id, "applied", `Applied via /evolve:apply: ${result.details}`);
1645
+ }
1646
+ console.log(JSON.stringify(result, null, 2));
1647
+ } catch (err) {
1648
+ console.log(JSON.stringify({
1649
+ recommendation_id: id,
1650
+ success: false,
1651
+ details: err instanceof Error ? err.message : String(err)
1652
+ }));
1653
+ process.exitCode = 1;
1654
+ }
1655
+ });
1656
+ }
1657
+ function registerDismissCommand(program2) {
1658
+ program2.command("dismiss").description("Permanently dismiss a recommendation by ID").argument("<id>", "Recommendation ID to dismiss").action(async (id) => {
1659
+ try {
1660
+ await updateStatus(id, "dismissed", "Dismissed by user via /evolve:apply");
1661
+ console.log(JSON.stringify({ id, status: "dismissed" }, null, 2));
1662
+ } catch (err) {
1663
+ console.log(JSON.stringify({
1664
+ id,
1665
+ error: err instanceof Error ? err.message : String(err)
1666
+ }));
1667
+ process.exitCode = 1;
1668
+ }
1669
+ });
1670
+ }
1671
+
1672
+ // src/cli.ts
1673
+ var pkg = JSON.parse(
1674
+ readFileSync(join10(import.meta.dirname, "..", "package.json"), "utf-8")
1675
+ );
1676
+ var program = new Command().name("harness-evolve").description("Self-iteration engine for Claude Code").version(pkg.version);
1677
+ registerInitCommand(program);
1678
+ registerStatusCommand(program);
1679
+ registerUninstallCommand(program);
1680
+ registerScanCommand(program);
1681
+ registerPendingCommand(program);
1682
+ registerApplyOneCommand(program);
1683
+ registerDismissCommand(program);
1684
+ program.parse();
1685
+ //# sourceMappingURL=cli.js.map