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.
@@ -0,0 +1,2069 @@
1
+ // src/storage/counter.ts
2
+ import { readFile } from "fs/promises";
3
+ import { lock } from "proper-lockfile";
4
+ import writeFileAtomic from "write-file-atomic";
5
+
6
+ // src/schemas/counter.ts
7
+ import { z } from "zod/v4";
8
+ var counterSchema = z.object({
9
+ total: z.number().default(0),
10
+ session: z.record(z.string(), z.number()).default({}),
11
+ last_analysis: z.iso.datetime().optional(),
12
+ last_updated: z.iso.datetime()
13
+ });
14
+
15
+ // src/storage/dirs.ts
16
+ import { mkdir } from "fs/promises";
17
+ import { join } from "path";
18
+ var BASE_DIR = join(process.env.HOME ?? "", ".harness-evolve");
19
+ var paths = {
20
+ base: BASE_DIR,
21
+ logs: {
22
+ prompts: join(BASE_DIR, "logs", "prompts"),
23
+ tools: join(BASE_DIR, "logs", "tools"),
24
+ permissions: join(BASE_DIR, "logs", "permissions"),
25
+ sessions: join(BASE_DIR, "logs", "sessions")
26
+ },
27
+ analysis: join(BASE_DIR, "analysis"),
28
+ analysisPreProcessed: join(BASE_DIR, "analysis", "pre-processed"),
29
+ summary: join(BASE_DIR, "analysis", "pre-processed", "summary.json"),
30
+ environmentSnapshot: join(BASE_DIR, "analysis", "environment-snapshot.json"),
31
+ analysisResult: join(BASE_DIR, "analysis", "analysis-result.json"),
32
+ pending: join(BASE_DIR, "pending"),
33
+ config: join(BASE_DIR, "config.json"),
34
+ counter: join(BASE_DIR, "counter.json"),
35
+ recommendations: join(BASE_DIR, "recommendations.md"),
36
+ recommendationState: join(BASE_DIR, "analysis", "recommendation-state.json"),
37
+ recommendationArchive: join(BASE_DIR, "analysis", "recommendations-archive"),
38
+ notificationFlag: join(BASE_DIR, "analysis", "has-pending-notifications"),
39
+ autoApplyLog: join(BASE_DIR, "analysis", "auto-apply-log.jsonl"),
40
+ outcomeHistory: join(BASE_DIR, "analysis", "outcome-history.jsonl")
41
+ };
42
+ var initialized = false;
43
+ async function ensureInit() {
44
+ if (initialized) return;
45
+ await mkdir(paths.logs.prompts, { recursive: true });
46
+ await mkdir(paths.logs.tools, { recursive: true });
47
+ await mkdir(paths.logs.permissions, { recursive: true });
48
+ await mkdir(paths.logs.sessions, { recursive: true });
49
+ await mkdir(paths.analysis, { recursive: true });
50
+ await mkdir(paths.analysisPreProcessed, { recursive: true });
51
+ await mkdir(paths.pending, { recursive: true });
52
+ await mkdir(paths.recommendationArchive, { recursive: true });
53
+ initialized = true;
54
+ }
55
+
56
+ // src/storage/config.ts
57
+ import { readFile as readFile2 } from "fs/promises";
58
+ import writeFileAtomic2 from "write-file-atomic";
59
+
60
+ // src/schemas/config.ts
61
+ import { z as z2 } from "zod/v4";
62
+ var configSchema = z2.object({
63
+ version: z2.number().default(1),
64
+ analysis: z2.object({
65
+ threshold: z2.number().min(1).default(50),
66
+ enabled: z2.boolean().default(true),
67
+ classifierThresholds: z2.record(z2.string(), z2.number()).default({})
68
+ }).default({ threshold: 50, enabled: true, classifierThresholds: {} }),
69
+ hooks: z2.object({
70
+ capturePrompts: z2.boolean().default(true),
71
+ captureTools: z2.boolean().default(true),
72
+ capturePermissions: z2.boolean().default(true),
73
+ captureSessions: z2.boolean().default(true)
74
+ }).default({
75
+ capturePrompts: true,
76
+ captureTools: true,
77
+ capturePermissions: true,
78
+ captureSessions: true
79
+ }),
80
+ scrubbing: z2.object({
81
+ enabled: z2.boolean().default(true),
82
+ highEntropyDetection: z2.boolean().default(false),
83
+ customPatterns: z2.array(z2.object({
84
+ name: z2.string(),
85
+ regex: z2.string(),
86
+ replacement: z2.string()
87
+ })).default([])
88
+ }).default({
89
+ enabled: true,
90
+ highEntropyDetection: false,
91
+ customPatterns: []
92
+ }),
93
+ delivery: z2.object({
94
+ stdoutInjection: z2.boolean().default(true),
95
+ maxTokens: z2.number().default(200),
96
+ fullAuto: z2.boolean().default(false),
97
+ maxRecommendationsInFile: z2.number().default(20),
98
+ archiveAfterDays: z2.number().default(7)
99
+ }).default({
100
+ stdoutInjection: true,
101
+ maxTokens: 200,
102
+ fullAuto: false,
103
+ maxRecommendationsInFile: 20,
104
+ archiveAfterDays: 7
105
+ })
106
+ }).strict();
107
+
108
+ // src/storage/config.ts
109
+ async function loadConfig() {
110
+ try {
111
+ const raw = await readFile2(paths.config, "utf-8");
112
+ return configSchema.parse(JSON.parse(raw));
113
+ } catch {
114
+ const defaults = configSchema.parse({});
115
+ await writeFileAtomic2(paths.config, JSON.stringify(defaults, null, 2));
116
+ return defaults;
117
+ }
118
+ }
119
+
120
+ // src/analysis/jsonl-reader.ts
121
+ import { createReadStream } from "fs";
122
+ import { readdir } from "fs/promises";
123
+ import { createInterface } from "readline";
124
+ import { join as join2 } from "path";
125
+ function formatDate(d) {
126
+ return d.toISOString().slice(0, 10);
127
+ }
128
+ async function readLogEntries(logDir, schema, options) {
129
+ let fileNames;
130
+ try {
131
+ fileNames = await readdir(logDir);
132
+ } catch {
133
+ return [];
134
+ }
135
+ let jsonlFiles = fileNames.filter((f) => f.endsWith(".jsonl"));
136
+ const sinceStr = options?.since ? formatDate(options.since) : void 0;
137
+ const untilStr = options?.until ? formatDate(options.until) : void 0;
138
+ if (sinceStr || untilStr) {
139
+ jsonlFiles = jsonlFiles.filter((f) => {
140
+ const dateStr = f.replace(".jsonl", "");
141
+ if (sinceStr && dateStr < sinceStr) return false;
142
+ if (untilStr && dateStr > untilStr) return false;
143
+ return true;
144
+ });
145
+ }
146
+ jsonlFiles.sort();
147
+ const entries = [];
148
+ for (const file of jsonlFiles) {
149
+ const filePath = join2(logDir, file);
150
+ const rl = createInterface({
151
+ input: createReadStream(filePath, "utf-8"),
152
+ crlfDelay: Infinity
153
+ });
154
+ for await (const line of rl) {
155
+ if (!line.trim()) continue;
156
+ try {
157
+ const parsed = schema.parse(JSON.parse(line));
158
+ entries.push(parsed);
159
+ } catch {
160
+ }
161
+ }
162
+ }
163
+ return entries;
164
+ }
165
+
166
+ // src/analysis/schemas.ts
167
+ import { z as z3 } from "zod/v4";
168
+ var summarySchema = z3.object({
169
+ generated_at: z3.iso.datetime(),
170
+ period: z3.object({
171
+ since: z3.string(),
172
+ // YYYY-MM-DD
173
+ until: z3.string(),
174
+ // YYYY-MM-DD
175
+ days: z3.number()
176
+ }),
177
+ stats: z3.object({
178
+ total_prompts: z3.number(),
179
+ total_tool_uses: z3.number(),
180
+ total_permissions: z3.number(),
181
+ unique_sessions: z3.number()
182
+ }),
183
+ top_repeated_prompts: z3.array(
184
+ z3.object({
185
+ prompt: z3.string(),
186
+ count: z3.number(),
187
+ sessions: z3.number()
188
+ })
189
+ ).max(20),
190
+ tool_frequency: z3.array(
191
+ z3.object({
192
+ tool_name: z3.string(),
193
+ count: z3.number(),
194
+ avg_duration_ms: z3.number().optional()
195
+ })
196
+ ),
197
+ permission_patterns: z3.array(
198
+ z3.object({
199
+ tool_name: z3.string(),
200
+ count: z3.number(),
201
+ sessions: z3.number()
202
+ })
203
+ ),
204
+ long_prompts: z3.array(
205
+ z3.object({
206
+ prompt_preview: z3.string(),
207
+ length: z3.number(),
208
+ count: z3.number()
209
+ })
210
+ ).max(10)
211
+ });
212
+ var environmentSnapshotSchema = z3.object({
213
+ generated_at: z3.iso.datetime(),
214
+ claude_code: z3.object({
215
+ version: z3.string(),
216
+ version_known: z3.boolean(),
217
+ compatible: z3.boolean()
218
+ }),
219
+ settings: z3.object({
220
+ user: z3.unknown().nullable(),
221
+ project: z3.unknown().nullable(),
222
+ local: z3.unknown().nullable()
223
+ }),
224
+ installed_tools: z3.object({
225
+ plugins: z3.array(
226
+ z3.object({
227
+ name: z3.string(),
228
+ marketplace: z3.string(),
229
+ enabled: z3.boolean(),
230
+ scope: z3.string(),
231
+ capabilities: z3.array(z3.string())
232
+ })
233
+ ),
234
+ skills: z3.array(
235
+ z3.object({
236
+ name: z3.string(),
237
+ scope: z3.enum(["user", "project"])
238
+ })
239
+ ),
240
+ rules: z3.array(
241
+ z3.object({
242
+ name: z3.string(),
243
+ scope: z3.enum(["user", "project"])
244
+ })
245
+ ),
246
+ hooks: z3.array(
247
+ z3.object({
248
+ event: z3.string(),
249
+ scope: z3.enum(["user", "project", "local"]),
250
+ type: z3.string()
251
+ })
252
+ ),
253
+ claude_md: z3.array(
254
+ z3.object({
255
+ path: z3.string(),
256
+ exists: z3.boolean()
257
+ })
258
+ )
259
+ }),
260
+ detected_ecosystems: z3.array(z3.string())
261
+ });
262
+
263
+ // src/schemas/log-entry.ts
264
+ import { z as z4 } from "zod/v4";
265
+ var promptEntrySchema = z4.object({
266
+ timestamp: z4.iso.datetime(),
267
+ session_id: z4.string(),
268
+ cwd: z4.string(),
269
+ prompt: z4.string(),
270
+ prompt_length: z4.number(),
271
+ transcript_path: z4.string().optional()
272
+ });
273
+ var toolEntrySchema = z4.object({
274
+ timestamp: z4.iso.datetime(),
275
+ session_id: z4.string(),
276
+ event: z4.enum(["pre", "post", "failure"]),
277
+ tool_name: z4.string(),
278
+ input_summary: z4.string().optional(),
279
+ duration_ms: z4.number().optional(),
280
+ success: z4.boolean().optional()
281
+ });
282
+ var permissionEntrySchema = z4.object({
283
+ timestamp: z4.iso.datetime(),
284
+ session_id: z4.string(),
285
+ tool_name: z4.string(),
286
+ decision: z4.enum(["approved", "denied", "unknown"])
287
+ });
288
+ var sessionEntrySchema = z4.object({
289
+ timestamp: z4.iso.datetime(),
290
+ session_id: z4.string(),
291
+ event: z4.enum(["start", "end"]),
292
+ cwd: z4.string().optional()
293
+ });
294
+
295
+ // src/analysis/pre-processor.ts
296
+ import writeFileAtomic3 from "write-file-atomic";
297
+ var PROMPT_TRUNCATE_LEN = 100;
298
+ var LONG_PROMPT_THRESHOLD = 200;
299
+ var DEFAULT_TOP_N = 20;
300
+ var DEFAULT_DAYS = 30;
301
+ var MAX_LONG_PROMPTS = 10;
302
+ function normalizePrompt(prompt) {
303
+ return prompt.trim().toLowerCase().replace(/\s+/g, " ");
304
+ }
305
+ function formatDate2(d) {
306
+ return d.toISOString().slice(0, 10);
307
+ }
308
+ function countWithSessions(items) {
309
+ const map = /* @__PURE__ */ new Map();
310
+ for (const { key, session } of items) {
311
+ const existing = map.get(key);
312
+ if (existing) {
313
+ existing.count += 1;
314
+ existing.sessions.add(session);
315
+ } else {
316
+ map.set(key, { count: 1, sessions: /* @__PURE__ */ new Set([session]) });
317
+ }
318
+ }
319
+ return map;
320
+ }
321
+ function computeToolFrequency(tools) {
322
+ const map = /* @__PURE__ */ new Map();
323
+ for (const entry of tools) {
324
+ const existing = map.get(entry.tool_name);
325
+ if (existing) {
326
+ existing.count += 1;
327
+ if (entry.event === "post" && entry.duration_ms != null) {
328
+ existing.durations.push(entry.duration_ms);
329
+ }
330
+ } else {
331
+ const durations = [];
332
+ if (entry.event === "post" && entry.duration_ms != null) {
333
+ durations.push(entry.duration_ms);
334
+ }
335
+ map.set(entry.tool_name, { count: 1, durations });
336
+ }
337
+ }
338
+ return Array.from(map.entries()).map(([tool_name, { count, durations }]) => ({
339
+ tool_name,
340
+ count,
341
+ avg_duration_ms: durations.length > 0 ? Math.round(
342
+ durations.reduce((sum, d) => sum + d, 0) / durations.length
343
+ ) : void 0
344
+ })).sort((a, b) => b.count - a.count);
345
+ }
346
+ function detectLongPrompts(prompts) {
347
+ const map = /* @__PURE__ */ new Map();
348
+ for (const entry of prompts) {
349
+ const words = entry.prompt.trim().split(/\s+/);
350
+ if (words.length <= LONG_PROMPT_THRESHOLD) continue;
351
+ const key = normalizePrompt(entry.prompt);
352
+ const existing = map.get(key);
353
+ if (existing) {
354
+ existing.count += 1;
355
+ } else {
356
+ map.set(key, { length: words.length, count: 1 });
357
+ }
358
+ }
359
+ return Array.from(map.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, MAX_LONG_PROMPTS).map(([normalized, { length, count }]) => ({
360
+ prompt_preview: normalized.slice(0, PROMPT_TRUNCATE_LEN),
361
+ length,
362
+ count
363
+ }));
364
+ }
365
+ async function preProcess(options) {
366
+ const until = options?.until ?? /* @__PURE__ */ new Date();
367
+ const since = options?.since ?? new Date(until.getTime() - DEFAULT_DAYS * 864e5);
368
+ const topN = options?.topN ?? DEFAULT_TOP_N;
369
+ const [prompts, tools, permissions] = await Promise.all([
370
+ readLogEntries(paths.logs.prompts, promptEntrySchema, { since, until }),
371
+ readLogEntries(paths.logs.tools, toolEntrySchema, { since, until }),
372
+ readLogEntries(paths.logs.permissions, permissionEntrySchema, {
373
+ since,
374
+ until
375
+ })
376
+ ]);
377
+ const sessionSet = /* @__PURE__ */ new Set();
378
+ for (const p of prompts) sessionSet.add(p.session_id);
379
+ for (const t of tools) sessionSet.add(t.session_id);
380
+ for (const perm of permissions) sessionSet.add(perm.session_id);
381
+ const promptCounts = countWithSessions(
382
+ prompts.map((p) => ({
383
+ key: normalizePrompt(p.prompt),
384
+ session: p.session_id
385
+ }))
386
+ );
387
+ const topRepeatedPrompts = Array.from(promptCounts.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, topN).map(([key, { count, sessions }]) => ({
388
+ prompt: key.slice(0, PROMPT_TRUNCATE_LEN),
389
+ count,
390
+ sessions: sessions.size
391
+ }));
392
+ const toolFrequency = computeToolFrequency(tools);
393
+ const permissionCounts = countWithSessions(
394
+ permissions.map((p) => ({
395
+ key: p.tool_name,
396
+ session: p.session_id
397
+ }))
398
+ );
399
+ const permissionPatterns = Array.from(permissionCounts.entries()).sort((a, b) => b[1].count - a[1].count).map(([tool_name, { count, sessions }]) => ({
400
+ tool_name,
401
+ count,
402
+ sessions: sessions.size
403
+ }));
404
+ const longPrompts = detectLongPrompts(prompts);
405
+ const summary = summarySchema.parse({
406
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
407
+ period: {
408
+ since: formatDate2(since),
409
+ until: formatDate2(until),
410
+ days: DEFAULT_DAYS
411
+ },
412
+ stats: {
413
+ total_prompts: prompts.length,
414
+ total_tool_uses: tools.length,
415
+ total_permissions: permissions.length,
416
+ unique_sessions: sessionSet.size
417
+ },
418
+ top_repeated_prompts: topRepeatedPrompts,
419
+ tool_frequency: toolFrequency,
420
+ permission_patterns: permissionPatterns,
421
+ long_prompts: longPrompts
422
+ });
423
+ await ensureInit();
424
+ await writeFileAtomic3(paths.summary, JSON.stringify(summary));
425
+ return summary;
426
+ }
427
+
428
+ // src/analysis/environment-scanner.ts
429
+ import { readdir as readdir2, readFile as readFile3, access } from "fs/promises";
430
+ import { execFileSync } from "child_process";
431
+ import { join as join3 } from "path";
432
+ import { constants } from "fs";
433
+ import writeFileAtomic4 from "write-file-atomic";
434
+ var KNOWN_COMPATIBLE_MIN = "2.1.0";
435
+ var KNOWN_COMPATIBLE_MAX = "2.1.99";
436
+ async function scanEnvironment(cwd, home) {
437
+ const homeDir = home ?? process.env.HOME ?? "";
438
+ const [userSettings, projectSettings, localSettings] = await Promise.all([
439
+ readSettingsSafe(join3(homeDir, ".claude", "settings.json")),
440
+ readSettingsSafe(join3(cwd, ".claude", "settings.json")),
441
+ readSettingsSafe(join3(cwd, ".claude", "settings.local.json"))
442
+ ]);
443
+ const enabledPluginNames = extractEnabledPlugins(userSettings);
444
+ const [claudeVersion, plugins, skills, rules, hooks, claudeMds, ecosystems] = await Promise.all([
445
+ Promise.resolve(detectClaudeCodeVersion()),
446
+ discoverPlugins(homeDir, enabledPluginNames),
447
+ discoverSkills(homeDir, cwd),
448
+ discoverRules(cwd),
449
+ Promise.resolve(
450
+ discoverHooks(userSettings, projectSettings, localSettings)
451
+ ),
452
+ discoverClaudeMd(homeDir, cwd),
453
+ detectEcosystems(cwd, homeDir)
454
+ ]);
455
+ const snapshot = {
456
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
457
+ claude_code: claudeVersion,
458
+ settings: {
459
+ user: userSettings,
460
+ project: projectSettings,
461
+ local: localSettings
462
+ },
463
+ installed_tools: {
464
+ plugins,
465
+ skills,
466
+ rules,
467
+ hooks,
468
+ claude_md: claudeMds
469
+ },
470
+ detected_ecosystems: ecosystems
471
+ };
472
+ const validated = environmentSnapshotSchema.parse(snapshot);
473
+ await ensureInit();
474
+ await writeFileAtomic4(
475
+ paths.environmentSnapshot,
476
+ JSON.stringify(validated)
477
+ );
478
+ return validated;
479
+ }
480
+ function detectClaudeCodeVersion() {
481
+ try {
482
+ const output = execFileSync("claude", ["--version"], {
483
+ timeout: 3e3,
484
+ encoding: "utf-8",
485
+ stdio: ["pipe", "pipe", "pipe"]
486
+ }).trim();
487
+ const match = output.match(/^(\d+\.\d+\.\d+)/);
488
+ if (!match) {
489
+ return { version: "unknown", version_known: false, compatible: false };
490
+ }
491
+ const version = match[1];
492
+ const compatible = compareSemver(version, KNOWN_COMPATIBLE_MIN) >= 0 && compareSemver(version, KNOWN_COMPATIBLE_MAX) <= 0;
493
+ return { version, version_known: true, compatible };
494
+ } catch {
495
+ return { version: "unknown", version_known: false, compatible: false };
496
+ }
497
+ }
498
+ async function readSettingsSafe(filePath) {
499
+ try {
500
+ const raw = await readFile3(filePath, "utf-8");
501
+ return JSON.parse(raw);
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ function extractEnabledPlugins(settings) {
507
+ if (!settings || typeof settings !== "object") return [];
508
+ const obj = settings;
509
+ if (!Array.isArray(obj.enabledPlugins)) return [];
510
+ return obj.enabledPlugins.map((p) => {
511
+ if (typeof p === "string") return p;
512
+ if (p && typeof p === "object" && "name" in p) {
513
+ return String(p.name);
514
+ }
515
+ return null;
516
+ }).filter((n) => n !== null);
517
+ }
518
+ async function discoverPlugins(home, enabledPluginNames) {
519
+ try {
520
+ const pluginsFile = join3(home, ".claude", "plugins", "installed_plugins.json");
521
+ const raw = await readFile3(pluginsFile, "utf-8");
522
+ const installed = JSON.parse(raw);
523
+ if (!Array.isArray(installed)) return [];
524
+ const plugins = [];
525
+ for (const entry of installed) {
526
+ if (!entry || typeof entry !== "object") continue;
527
+ const plugin = entry;
528
+ const name = String(plugin.name ?? "");
529
+ const marketplace = String(plugin.marketplace ?? "unknown");
530
+ const scope = String(plugin.scope ?? "user");
531
+ const version = String(plugin.version ?? "latest");
532
+ const enabled = enabledPluginNames.includes(name);
533
+ const capabilities = await scanPluginCapabilities(
534
+ home,
535
+ marketplace,
536
+ name,
537
+ version
538
+ );
539
+ plugins.push({ name, marketplace, enabled, scope, capabilities });
540
+ }
541
+ return plugins;
542
+ } catch {
543
+ return [];
544
+ }
545
+ }
546
+ async function scanPluginCapabilities(home, marketplace, pluginName, version) {
547
+ const knownCapabilities = ["commands", "skills", "hooks", "agents"];
548
+ const capabilities = [];
549
+ try {
550
+ const cacheDir = join3(
551
+ home,
552
+ ".claude",
553
+ "plugins",
554
+ "cache",
555
+ marketplace,
556
+ pluginName,
557
+ version
558
+ );
559
+ const entries = await readdir2(cacheDir, { withFileTypes: true });
560
+ for (const entry of entries) {
561
+ if (entry.isDirectory() && knownCapabilities.includes(entry.name)) {
562
+ capabilities.push(entry.name);
563
+ }
564
+ }
565
+ } catch {
566
+ }
567
+ return capabilities;
568
+ }
569
+ async function discoverSkills(home, cwd) {
570
+ const skills = [];
571
+ try {
572
+ const userSkillsDir = join3(home, ".claude", "skills");
573
+ const entries = await readdir2(userSkillsDir, { withFileTypes: true });
574
+ for (const entry of entries) {
575
+ if (entry.isDirectory()) {
576
+ skills.push({ name: entry.name, scope: "user" });
577
+ }
578
+ }
579
+ } catch {
580
+ }
581
+ try {
582
+ const projectSkillsDir = join3(cwd, ".claude", "skills");
583
+ const entries = await readdir2(projectSkillsDir, { withFileTypes: true });
584
+ for (const entry of entries) {
585
+ if (entry.isDirectory()) {
586
+ skills.push({ name: entry.name, scope: "project" });
587
+ }
588
+ }
589
+ } catch {
590
+ }
591
+ return skills;
592
+ }
593
+ async function discoverRules(cwd) {
594
+ const rules = [];
595
+ try {
596
+ const rulesDir = join3(cwd, ".claude", "rules");
597
+ const entries = await readdir2(rulesDir, { withFileTypes: true });
598
+ for (const entry of entries) {
599
+ if (entry.isDirectory()) {
600
+ rules.push({ name: entry.name, scope: "project" });
601
+ }
602
+ }
603
+ } catch {
604
+ }
605
+ return rules;
606
+ }
607
+ function discoverHooks(userSettings, projectSettings, localSettings) {
608
+ const hooks = [];
609
+ extractHooksFromSettings(userSettings, "user", hooks);
610
+ extractHooksFromSettings(projectSettings, "project", hooks);
611
+ extractHooksFromSettings(localSettings, "local", hooks);
612
+ return hooks;
613
+ }
614
+ function extractHooksFromSettings(settings, scope, hooks) {
615
+ if (!settings || typeof settings !== "object") return;
616
+ const obj = settings;
617
+ if (!obj.hooks || typeof obj.hooks !== "object") return;
618
+ const hooksConfig = obj.hooks;
619
+ for (const [event, defs] of Object.entries(hooksConfig)) {
620
+ if (!Array.isArray(defs)) continue;
621
+ for (const def of defs) {
622
+ if (!def || typeof def !== "object") continue;
623
+ const hookDef = def;
624
+ const type = String(hookDef.type ?? "command");
625
+ hooks.push({ event, scope, type });
626
+ }
627
+ }
628
+ }
629
+ async function discoverClaudeMd(home, cwd) {
630
+ const locations = [
631
+ join3(cwd, "CLAUDE.md"),
632
+ join3(cwd, ".claude", "CLAUDE.md"),
633
+ join3(home, ".claude", "CLAUDE.md")
634
+ ];
635
+ const results = [];
636
+ for (const path of locations) {
637
+ let exists = false;
638
+ try {
639
+ await access(path, constants.F_OK);
640
+ exists = true;
641
+ } catch {
642
+ }
643
+ results.push({ path, exists });
644
+ }
645
+ return results;
646
+ }
647
+ async function detectEcosystems(cwd, home) {
648
+ const ecosystems = [];
649
+ try {
650
+ await access(join3(cwd, ".planning"), constants.F_OK);
651
+ ecosystems.push("gsd");
652
+ } catch {
653
+ }
654
+ try {
655
+ const skillsDir = join3(home, ".claude", "skills");
656
+ const entries = await readdir2(skillsDir);
657
+ if (entries.some((e) => e.toLowerCase().includes("cog"))) {
658
+ ecosystems.push("cog");
659
+ }
660
+ } catch {
661
+ }
662
+ return ecosystems;
663
+ }
664
+ function compareSemver(a, b) {
665
+ const partsA = a.split(".").map(Number);
666
+ const partsB = b.split(".").map(Number);
667
+ for (let i = 0; i < 3; i++) {
668
+ const numA = partsA[i] ?? 0;
669
+ const numB = partsB[i] ?? 0;
670
+ if (numA < numB) return -1;
671
+ if (numA > numB) return 1;
672
+ }
673
+ return 0;
674
+ }
675
+
676
+ // src/analysis/classifiers/repeated-prompts.ts
677
+ function truncate(str, maxLen) {
678
+ if (str.length <= maxLen) return str;
679
+ return str.slice(0, maxLen - 3) + "...";
680
+ }
681
+ function classifyRepeatedPrompts(summary, _snapshot, config) {
682
+ const recommendations = [];
683
+ const threshold = config.thresholds.repeated_prompt_min_count;
684
+ for (let i = 0; i < summary.top_repeated_prompts.length; i++) {
685
+ const entry = summary.top_repeated_prompts[i];
686
+ if (entry.count < threshold) continue;
687
+ const wordCount = entry.prompt.split(/\s+/).length;
688
+ if (wordCount > 50) continue;
689
+ const confidence = entry.count >= config.thresholds.repeated_prompt_high_count && entry.sessions >= config.thresholds.repeated_prompt_high_sessions ? "HIGH" : "MEDIUM";
690
+ const truncatedPrompt = truncate(entry.prompt, 60);
691
+ recommendations.push({
692
+ id: `rec-repeated-${i}`,
693
+ target: "HOOK",
694
+ confidence,
695
+ pattern_type: "repeated_prompt",
696
+ title: `Repeated prompt: "${truncatedPrompt}"`,
697
+ description: `This prompt has been used ${entry.count} times across ${entry.sessions} sessions. Consider creating a hook or alias to automate this.`,
698
+ evidence: {
699
+ count: entry.count,
700
+ sessions: entry.sessions,
701
+ examples: [entry.prompt]
702
+ },
703
+ suggested_action: `Create a UserPromptSubmit hook that detects "${truncatedPrompt}" and auto-executes the intended action.`
704
+ });
705
+ }
706
+ return recommendations;
707
+ }
708
+
709
+ // src/analysis/classifiers/long-prompts.ts
710
+ function classifyLongPrompts(summary, _snapshot, config) {
711
+ const recommendations = [];
712
+ for (let i = 0; i < summary.long_prompts.length; i++) {
713
+ const entry = summary.long_prompts[i];
714
+ if (entry.length < config.thresholds.long_prompt_min_words) continue;
715
+ if (entry.count < config.thresholds.long_prompt_min_count) continue;
716
+ const confidence = entry.count >= config.thresholds.long_prompt_high_count && entry.length >= config.thresholds.long_prompt_high_words ? "HIGH" : "MEDIUM";
717
+ recommendations.push({
718
+ id: `rec-long-${i}`,
719
+ target: "SKILL",
720
+ confidence,
721
+ pattern_type: "long_prompt",
722
+ title: `Long repeated prompt (${entry.length} words, ${entry.count}x)`,
723
+ description: `A ${entry.length}-word prompt has been used ${entry.count} times. Consider converting it to a reusable skill.`,
724
+ evidence: {
725
+ count: entry.count,
726
+ examples: [entry.prompt_preview]
727
+ },
728
+ suggested_action: "Create a skill in .claude/skills/ that encapsulates this prompt as a reusable workflow."
729
+ });
730
+ }
731
+ return recommendations;
732
+ }
733
+
734
+ // src/analysis/classifiers/permission-patterns.ts
735
+ function classifyPermissionPatterns(summary, _snapshot, config) {
736
+ const recommendations = [];
737
+ for (let i = 0; i < summary.permission_patterns.length; i++) {
738
+ const entry = summary.permission_patterns[i];
739
+ if (entry.count < config.thresholds.permission_approval_min_count) continue;
740
+ if (entry.sessions < config.thresholds.permission_approval_min_sessions) continue;
741
+ const confidence = entry.count >= config.thresholds.permission_approval_high_count && entry.sessions >= config.thresholds.permission_approval_high_sessions ? "HIGH" : "MEDIUM";
742
+ recommendations.push({
743
+ id: `rec-permission-always-approved-${i}`,
744
+ target: "SETTINGS",
745
+ confidence,
746
+ pattern_type: "permission-always-approved",
747
+ title: `Frequently approved tool: ${entry.tool_name}`,
748
+ description: `You have approved "${entry.tool_name}" ${entry.count} times across ${entry.sessions} sessions. Consider adding it to allowedTools in settings.json.`,
749
+ evidence: {
750
+ count: entry.count,
751
+ sessions: entry.sessions,
752
+ examples: [`${entry.tool_name} approved ${entry.count} times`]
753
+ },
754
+ suggested_action: `Add "${entry.tool_name}" to the "allow" array in ~/.claude/settings.json permissions.`
755
+ });
756
+ }
757
+ return recommendations;
758
+ }
759
+
760
+ // src/analysis/classifiers/code-corrections.ts
761
+ var CODE_MODIFICATION_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
762
+ var HIGH_USAGE_THRESHOLD = 20;
763
+ function classifyCodeCorrections(summary, _snapshot, _config) {
764
+ const recommendations = [];
765
+ let index = 0;
766
+ for (const entry of summary.tool_frequency) {
767
+ if (!CODE_MODIFICATION_TOOLS.has(entry.tool_name)) continue;
768
+ if (entry.count < HIGH_USAGE_THRESHOLD) continue;
769
+ recommendations.push({
770
+ id: `rec-correction-${index}`,
771
+ target: "RULE",
772
+ confidence: "LOW",
773
+ pattern_type: "code_correction",
774
+ title: `Frequent code modifications with ${entry.tool_name} (${entry.count} uses)`,
775
+ description: `The ${entry.tool_name} tool has been used ${entry.count} times. Review for recurring patterns that could become a coding rule or convention.`,
776
+ evidence: {
777
+ count: entry.count,
778
+ examples: [entry.tool_name]
779
+ },
780
+ suggested_action: `Review recent ${entry.tool_name} usage for recurring patterns. If a consistent code style or approach emerges, create a rule in .claude/rules/.`
781
+ });
782
+ index++;
783
+ }
784
+ return recommendations;
785
+ }
786
+
787
+ // src/analysis/classifiers/personal-info.ts
788
+ var PERSONAL_KEYWORDS = [
789
+ "my name is",
790
+ "i live in",
791
+ "i work at",
792
+ "i prefer",
793
+ "my email",
794
+ "my project",
795
+ "always use",
796
+ "never use"
797
+ ];
798
+ var MIN_COUNT = 2;
799
+ function classifyPersonalInfo(summary, _snapshot, _config) {
800
+ const recommendations = [];
801
+ const matchedKeywords = /* @__PURE__ */ new Set();
802
+ let index = 0;
803
+ for (const entry of summary.top_repeated_prompts) {
804
+ if (entry.count < MIN_COUNT) continue;
805
+ const lowerPrompt = entry.prompt.toLowerCase();
806
+ for (const keyword of PERSONAL_KEYWORDS) {
807
+ if (matchedKeywords.has(keyword)) continue;
808
+ if (!lowerPrompt.includes(keyword)) continue;
809
+ matchedKeywords.add(keyword);
810
+ recommendations.push({
811
+ id: `rec-personal-${index}`,
812
+ target: "MEMORY",
813
+ confidence: "LOW",
814
+ pattern_type: "personal_info",
815
+ title: `Personal preference detected: "${keyword}..."`,
816
+ description: `A prompt mentioning personal information ("${keyword}") has appeared ${entry.count} times. Consider storing this in memory for automatic context.`,
817
+ evidence: {
818
+ count: entry.count,
819
+ sessions: entry.sessions,
820
+ examples: [entry.prompt]
821
+ },
822
+ suggested_action: "Add this information to memory (e.g., CLAUDE.md or a memory file) so Claude Code can use it without being reminded."
823
+ });
824
+ index++;
825
+ }
826
+ }
827
+ return recommendations;
828
+ }
829
+
830
+ // src/analysis/classifiers/config-drift.ts
831
+ var MAX_HOOKS_BEFORE_REVIEW = 10;
832
+ function classifyConfigDrift(_summary, snapshot, _config) {
833
+ const recommendations = [];
834
+ let index = 0;
835
+ const hookEvents = new Set(snapshot.installed_tools.hooks.map((h) => h.event));
836
+ const ruleNames = new Set(snapshot.installed_tools.rules.map((r) => r.name));
837
+ for (const overlap of hookEvents) {
838
+ if (!ruleNames.has(overlap)) continue;
839
+ recommendations.push({
840
+ id: `rec-drift-${index}`,
841
+ target: "RULE",
842
+ confidence: "LOW",
843
+ pattern_type: "config_drift",
844
+ title: `Hook-rule overlap detected: "${overlap}"`,
845
+ description: `Both a hook (event: "${overlap}") and a rule (name: "${overlap}") exist. This may indicate duplicated behavior that should be consolidated into one mechanism.`,
846
+ evidence: {
847
+ count: 2,
848
+ examples: [`Hook event: ${overlap}`, `Rule name: ${overlap}`]
849
+ },
850
+ suggested_action: `Review the hook and rule for "${overlap}". If they serve the same purpose, consolidate into the more appropriate mechanism (hooks for 100% reliability, rules for guidance).`
851
+ });
852
+ index++;
853
+ }
854
+ const existingClaudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists);
855
+ if (existingClaudeMd.length > 1) {
856
+ recommendations.push({
857
+ id: `rec-drift-${index}`,
858
+ target: "CLAUDE_MD",
859
+ confidence: "LOW",
860
+ pattern_type: "config_drift",
861
+ title: `Multiple CLAUDE.md files detected (${existingClaudeMd.length})`,
862
+ description: `Found ${existingClaudeMd.length} existing CLAUDE.md files. Multiple CLAUDE.md files may contain contradictory instructions. Review for consistency.`,
863
+ evidence: {
864
+ count: existingClaudeMd.length,
865
+ examples: existingClaudeMd.slice(0, 3).map((c) => c.path)
866
+ },
867
+ suggested_action: "Review all CLAUDE.md files for contradictions or redundancies. Consider consolidating shared instructions into the most appropriate scope."
868
+ });
869
+ index++;
870
+ }
871
+ if (snapshot.installed_tools.hooks.length > MAX_HOOKS_BEFORE_REVIEW) {
872
+ recommendations.push({
873
+ id: `rec-drift-${index}`,
874
+ target: "HOOK",
875
+ confidence: "LOW",
876
+ pattern_type: "config_drift",
877
+ title: `Excessive hook count (${snapshot.installed_tools.hooks.length} hooks)`,
878
+ description: `Found ${snapshot.installed_tools.hooks.length} hooks across all scopes. This many hooks may indicate redundancy or performance concerns. Review for consolidation.`,
879
+ evidence: {
880
+ count: snapshot.installed_tools.hooks.length,
881
+ examples: snapshot.installed_tools.hooks.slice(0, 3).map((h) => `${h.event} (${h.scope})`)
882
+ },
883
+ suggested_action: "Review all hooks for overlapping functionality. Consider combining hooks that trigger on the same event or serve similar purposes."
884
+ });
885
+ }
886
+ return recommendations;
887
+ }
888
+
889
+ // src/analysis/classifiers/ecosystem-adapter.ts
890
+ var MULTI_STEP_MIN_COUNT = 3;
891
+ function classifyEcosystemAdaptations(summary, snapshot, _config) {
892
+ const recommendations = [];
893
+ let index = 0;
894
+ if (snapshot.claude_code.version_known && !snapshot.claude_code.compatible && snapshot.claude_code.version !== "unknown") {
895
+ recommendations.push({
896
+ id: `rec-ecosystem-${index}`,
897
+ target: "CLAUDE_MD",
898
+ confidence: "MEDIUM",
899
+ pattern_type: "version_update",
900
+ title: `Claude Code version ${snapshot.claude_code.version} detected (outside tested range)`,
901
+ description: `Your Claude Code version (${snapshot.claude_code.version}) is outside the tested compatible range. New features may be available that change optimal configuration strategies.`,
902
+ evidence: {
903
+ count: 1,
904
+ examples: [`Version: ${snapshot.claude_code.version}`]
905
+ },
906
+ suggested_action: `Review Claude Code changelog for version ${snapshot.claude_code.version}. New hook events, permission models, or settings may require harness-evolve configuration updates.`
907
+ });
908
+ index++;
909
+ }
910
+ if (snapshot.detected_ecosystems.includes("gsd")) {
911
+ const multiStepPrompts = summary.top_repeated_prompts.filter(
912
+ (p) => p.count >= MULTI_STEP_MIN_COUNT
913
+ );
914
+ if (multiStepPrompts.length > 0) {
915
+ const topPrompt = multiStepPrompts[0];
916
+ recommendations.push({
917
+ id: `rec-ecosystem-${index}`,
918
+ target: "SKILL",
919
+ confidence: "LOW",
920
+ pattern_type: "ecosystem_gsd",
921
+ title: "GSD workflow detected -- consider /gsd slash commands",
922
+ description: "GSD is installed in this project. Repeated multi-step prompts may be better served by GSD planning phases or slash commands rather than standalone skills.",
923
+ evidence: {
924
+ count: multiStepPrompts.length,
925
+ examples: [topPrompt.prompt]
926
+ },
927
+ suggested_action: "Review repeated prompts and consider if they map to GSD phases (/gsd:plan-phase, /gsd:execute-phase) or custom slash commands.",
928
+ ecosystem_context: "GSD detected: Use /gsd slash commands and .planning patterns for multi-step workflows instead of standalone skills"
929
+ });
930
+ index++;
931
+ }
932
+ }
933
+ if (snapshot.detected_ecosystems.includes("cog")) {
934
+ recommendations.push({
935
+ id: `rec-ecosystem-${index}`,
936
+ target: "MEMORY",
937
+ confidence: "LOW",
938
+ pattern_type: "ecosystem_cog",
939
+ title: "Cog memory system detected -- route memory to Cog tiers",
940
+ description: "Cog is installed. Personal information and contextual preferences should be routed to Cog's tiered memory system rather than raw CLAUDE.md entries.",
941
+ evidence: {
942
+ count: 1,
943
+ examples: ["Cog detected in ~/.claude/skills/"]
944
+ },
945
+ suggested_action: "Use /reflect and /evolve Cog commands for memory management instead of manually editing CLAUDE.md.",
946
+ ecosystem_context: "Cog detected: Route memory entries to Cog tiers (/reflect, /evolve) instead of raw CLAUDE.md"
947
+ });
948
+ }
949
+ return recommendations;
950
+ }
951
+
952
+ // src/analysis/experience-level.ts
953
+ function computeExperienceLevel(snapshot) {
954
+ const hooks = snapshot.installed_tools.hooks.length;
955
+ const rules = snapshot.installed_tools.rules.length;
956
+ const skills = snapshot.installed_tools.skills.length;
957
+ const plugins = snapshot.installed_tools.plugins.length;
958
+ const claudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists).length;
959
+ const ecosystems = snapshot.detected_ecosystems.length;
960
+ const score = Math.min(
961
+ 100,
962
+ hooks * 8 + rules * 6 + skills * 5 + plugins * 10 + claudeMd * 3 + ecosystems * 7
963
+ );
964
+ const tier = score === 0 ? "newcomer" : score < 30 ? "intermediate" : "power_user";
965
+ return {
966
+ tier,
967
+ score,
968
+ breakdown: { hooks, rules, skills, plugins, claude_md: claudeMd, ecosystems }
969
+ };
970
+ }
971
+
972
+ // src/analysis/classifiers/onboarding.ts
973
+ function classifyOnboarding(_summary, snapshot, _config) {
974
+ const level = computeExperienceLevel(snapshot);
975
+ const recommendations = [];
976
+ let index = 0;
977
+ if (level.tier === "newcomer") {
978
+ if (level.breakdown.hooks === 0) {
979
+ recommendations.push({
980
+ id: `rec-onboarding-${index}`,
981
+ target: "HOOK",
982
+ confidence: "MEDIUM",
983
+ pattern_type: "onboarding_start_hooks",
984
+ title: "Start automating: create your first hook",
985
+ description: "Hooks run automatically on Claude Code lifecycle events (pre-commit, tool use, session start). Start with a formatting or test-on-save hook to experience automation benefits.",
986
+ evidence: {
987
+ count: 0,
988
+ examples: ["No hooks detected in your environment"]
989
+ },
990
+ suggested_action: "Add a hook in .claude/settings.json hooks section for automation."
991
+ });
992
+ index++;
993
+ }
994
+ if (level.breakdown.rules === 0) {
995
+ recommendations.push({
996
+ id: `rec-onboarding-${index}`,
997
+ target: "RULE",
998
+ confidence: "MEDIUM",
999
+ pattern_type: "onboarding_start_rules",
1000
+ title: "Define coding preferences: add your first rule",
1001
+ description: "Rules (.claude/rules/) codify conventions that Claude follows automatically. They persist across sessions and ensure consistent behavior.",
1002
+ evidence: {
1003
+ count: 0,
1004
+ examples: ["No rules detected in your environment"]
1005
+ },
1006
+ suggested_action: "Create .claude/rules/ directory with a rule for your preferred coding style."
1007
+ });
1008
+ index++;
1009
+ }
1010
+ if (level.breakdown.claude_md === 0) {
1011
+ recommendations.push({
1012
+ id: `rec-onboarding-${index}`,
1013
+ target: "CLAUDE_MD",
1014
+ confidence: "MEDIUM",
1015
+ pattern_type: "onboarding_start_claudemd",
1016
+ title: "Set project context: create CLAUDE.md",
1017
+ description: "CLAUDE.md gives Claude project-specific context \u2014 tech stack, conventions, and constraints. It is loaded automatically at the start of every conversation.",
1018
+ evidence: {
1019
+ count: 0,
1020
+ examples: ["No CLAUDE.md files detected in your environment"]
1021
+ },
1022
+ suggested_action: "Create CLAUDE.md in your project root with project description and conventions."
1023
+ });
1024
+ }
1025
+ } else if (level.tier === "power_user") {
1026
+ recommendations.push({
1027
+ id: "rec-onboarding-3",
1028
+ target: "SETTINGS",
1029
+ confidence: "LOW",
1030
+ pattern_type: "onboarding_optimize",
1031
+ title: "Consider mechanizing recurring patterns",
1032
+ description: "Your extensive configuration suggests active automation investment. Review for redundancy or upgrade opportunities \u2014 hooks and rules with overlapping concerns can be consolidated.",
1033
+ evidence: {
1034
+ count: level.score,
1035
+ examples: [
1036
+ `${level.breakdown.hooks} hooks, ${level.breakdown.rules} rules, ${level.breakdown.plugins} plugins installed`
1037
+ ]
1038
+ },
1039
+ suggested_action: "Review your hooks and rules for overlapping concerns that could be consolidated."
1040
+ });
1041
+ }
1042
+ return recommendations;
1043
+ }
1044
+
1045
+ // src/analysis/classifiers/index.ts
1046
+ var classifiers = [
1047
+ classifyRepeatedPrompts,
1048
+ classifyLongPrompts,
1049
+ classifyPermissionPatterns,
1050
+ classifyCodeCorrections,
1051
+ classifyPersonalInfo,
1052
+ classifyConfigDrift,
1053
+ classifyEcosystemAdaptations,
1054
+ classifyOnboarding
1055
+ ];
1056
+
1057
+ // src/schemas/recommendation.ts
1058
+ import { z as z5 } from "zod/v4";
1059
+ var routingTargetSchema = z5.enum([
1060
+ "HOOK",
1061
+ "SKILL",
1062
+ "RULE",
1063
+ "CLAUDE_MD",
1064
+ "MEMORY",
1065
+ "SETTINGS"
1066
+ ]);
1067
+ var confidenceSchema = z5.enum(["HIGH", "MEDIUM", "LOW"]);
1068
+ var patternTypeSchema = z5.enum([
1069
+ "repeated_prompt",
1070
+ "long_prompt",
1071
+ "permission-always-approved",
1072
+ "code_correction",
1073
+ "personal_info",
1074
+ "config_drift",
1075
+ "version_update",
1076
+ "ecosystem_gsd",
1077
+ "ecosystem_cog",
1078
+ "onboarding_start_hooks",
1079
+ "onboarding_start_rules",
1080
+ "onboarding_start_claudemd",
1081
+ "onboarding_optimize",
1082
+ "scan_redundancy",
1083
+ "scan_missing_mechanization",
1084
+ "scan_stale_reference"
1085
+ ]);
1086
+ var recommendationSchema = z5.object({
1087
+ id: z5.string(),
1088
+ target: routingTargetSchema,
1089
+ confidence: confidenceSchema,
1090
+ pattern_type: patternTypeSchema,
1091
+ title: z5.string(),
1092
+ description: z5.string(),
1093
+ evidence: z5.object({
1094
+ count: z5.number(),
1095
+ sessions: z5.number().optional(),
1096
+ examples: z5.array(z5.string()).max(3)
1097
+ }),
1098
+ suggested_action: z5.string(),
1099
+ ecosystem_context: z5.string().optional()
1100
+ });
1101
+ var DEFAULT_THRESHOLDS = {
1102
+ repeated_prompt_min_count: 5,
1103
+ repeated_prompt_high_count: 10,
1104
+ repeated_prompt_high_sessions: 3,
1105
+ repeated_prompt_medium_sessions: 2,
1106
+ long_prompt_min_words: 200,
1107
+ long_prompt_min_count: 2,
1108
+ long_prompt_high_words: 300,
1109
+ long_prompt_high_count: 3,
1110
+ permission_approval_min_count: 10,
1111
+ permission_approval_min_sessions: 3,
1112
+ permission_approval_high_count: 15,
1113
+ permission_approval_high_sessions: 4,
1114
+ code_correction_min_failure_rate: 0.3,
1115
+ code_correction_min_failures: 3
1116
+ };
1117
+ var analysisConfigSchema = z5.object({
1118
+ thresholds: z5.object({
1119
+ repeated_prompt_min_count: z5.number().default(DEFAULT_THRESHOLDS.repeated_prompt_min_count),
1120
+ repeated_prompt_high_count: z5.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_count),
1121
+ repeated_prompt_high_sessions: z5.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_sessions),
1122
+ repeated_prompt_medium_sessions: z5.number().default(DEFAULT_THRESHOLDS.repeated_prompt_medium_sessions),
1123
+ long_prompt_min_words: z5.number().default(DEFAULT_THRESHOLDS.long_prompt_min_words),
1124
+ long_prompt_min_count: z5.number().default(DEFAULT_THRESHOLDS.long_prompt_min_count),
1125
+ long_prompt_high_words: z5.number().default(DEFAULT_THRESHOLDS.long_prompt_high_words),
1126
+ long_prompt_high_count: z5.number().default(DEFAULT_THRESHOLDS.long_prompt_high_count),
1127
+ permission_approval_min_count: z5.number().default(DEFAULT_THRESHOLDS.permission_approval_min_count),
1128
+ permission_approval_min_sessions: z5.number().default(DEFAULT_THRESHOLDS.permission_approval_min_sessions),
1129
+ permission_approval_high_count: z5.number().default(DEFAULT_THRESHOLDS.permission_approval_high_count),
1130
+ permission_approval_high_sessions: z5.number().default(DEFAULT_THRESHOLDS.permission_approval_high_sessions),
1131
+ code_correction_min_failure_rate: z5.number().default(DEFAULT_THRESHOLDS.code_correction_min_failure_rate),
1132
+ code_correction_min_failures: z5.number().default(DEFAULT_THRESHOLDS.code_correction_min_failures)
1133
+ }).default(() => ({ ...DEFAULT_THRESHOLDS })),
1134
+ max_recommendations: z5.number().default(20)
1135
+ }).default(() => ({
1136
+ thresholds: { ...DEFAULT_THRESHOLDS },
1137
+ max_recommendations: 20
1138
+ }));
1139
+ var analysisResultSchema = z5.object({
1140
+ generated_at: z5.iso.datetime(),
1141
+ summary_period: z5.object({
1142
+ since: z5.string(),
1143
+ until: z5.string(),
1144
+ days: z5.number()
1145
+ }),
1146
+ recommendations: z5.array(recommendationSchema),
1147
+ metadata: z5.object({
1148
+ classifier_count: z5.number(),
1149
+ patterns_evaluated: z5.number(),
1150
+ environment_ecosystems: z5.array(z5.string()),
1151
+ claude_code_version: z5.string()
1152
+ })
1153
+ });
1154
+
1155
+ // src/analysis/analyzer.ts
1156
+ var CONFIDENCE_ORDER = {
1157
+ HIGH: 0,
1158
+ MEDIUM: 1,
1159
+ LOW: 2
1160
+ };
1161
+ function sortRecommendations(a, b) {
1162
+ const confDiff = (CONFIDENCE_ORDER[a.confidence] ?? 3) - (CONFIDENCE_ORDER[b.confidence] ?? 3);
1163
+ if (confDiff !== 0) return confDiff;
1164
+ return b.evidence.count - a.evidence.count;
1165
+ }
1166
+ function adjustConfidence(recommendations, summaries) {
1167
+ const rateByType = new Map(
1168
+ summaries.map((s) => [s.pattern_type, s.persistence_rate])
1169
+ );
1170
+ return recommendations.map((rec) => {
1171
+ const rate = rateByType.get(rec.pattern_type);
1172
+ if (rate === void 0 || rate >= 0.7) return rec;
1173
+ const downgraded = {
1174
+ HIGH: "MEDIUM",
1175
+ MEDIUM: "LOW",
1176
+ LOW: "LOW"
1177
+ };
1178
+ return {
1179
+ ...rec,
1180
+ confidence: downgraded[rec.confidence] ?? rec.confidence
1181
+ };
1182
+ });
1183
+ }
1184
+ function analyze(summary, snapshot, config, outcomeSummaries) {
1185
+ const mergedConfig = config ?? analysisConfigSchema.parse({});
1186
+ const recommendations = [];
1187
+ for (const classify of classifiers) {
1188
+ const results = classify(summary, snapshot, mergedConfig);
1189
+ recommendations.push(...results);
1190
+ }
1191
+ const adjusted = outcomeSummaries && outcomeSummaries.length > 0 ? adjustConfidence(recommendations, outcomeSummaries) : recommendations;
1192
+ adjusted.sort(sortRecommendations);
1193
+ const capped = adjusted.slice(0, mergedConfig.max_recommendations);
1194
+ const patternsEvaluated = summary.top_repeated_prompts.length + summary.long_prompts.length + summary.permission_patterns.length + summary.tool_frequency.length;
1195
+ return analysisResultSchema.parse({
1196
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1197
+ summary_period: summary.period,
1198
+ recommendations: capped,
1199
+ metadata: {
1200
+ classifier_count: classifiers.length,
1201
+ patterns_evaluated: patternsEvaluated,
1202
+ environment_ecosystems: snapshot.detected_ecosystems,
1203
+ claude_code_version: snapshot.claude_code.version
1204
+ }
1205
+ });
1206
+ }
1207
+
1208
+ // src/analysis/outcome-tracker.ts
1209
+ import { readFile as readFile5, appendFile } from "fs/promises";
1210
+
1211
+ // src/delivery/state.ts
1212
+ import { readFile as readFile4 } from "fs/promises";
1213
+ import writeFileAtomic5 from "write-file-atomic";
1214
+
1215
+ // src/schemas/delivery.ts
1216
+ import { z as z6 } from "zod/v4";
1217
+ var recommendationStatusSchema = z6.enum(["pending", "applied", "dismissed"]);
1218
+ var recommendationStateEntrySchema = z6.object({
1219
+ id: z6.string(),
1220
+ status: recommendationStatusSchema,
1221
+ updated_at: z6.iso.datetime(),
1222
+ applied_details: z6.string().optional()
1223
+ });
1224
+ var recommendationStateSchema = z6.object({
1225
+ entries: z6.array(recommendationStateEntrySchema),
1226
+ last_updated: z6.iso.datetime()
1227
+ });
1228
+ var autoApplyLogEntrySchema = z6.object({
1229
+ timestamp: z6.iso.datetime(),
1230
+ recommendation_id: z6.string(),
1231
+ target: z6.string(),
1232
+ action: z6.string(),
1233
+ success: z6.boolean(),
1234
+ details: z6.string().optional(),
1235
+ backup_path: z6.string().optional()
1236
+ });
1237
+
1238
+ // src/delivery/state.ts
1239
+ async function loadState() {
1240
+ try {
1241
+ const raw = await readFile4(paths.recommendationState, "utf-8");
1242
+ return recommendationStateSchema.parse(JSON.parse(raw));
1243
+ } catch (err) {
1244
+ if (isNodeError(err) && err.code === "ENOENT") {
1245
+ return { entries: [], last_updated: (/* @__PURE__ */ new Date()).toISOString() };
1246
+ }
1247
+ throw err;
1248
+ }
1249
+ }
1250
+ async function saveState(state) {
1251
+ await writeFileAtomic5(
1252
+ paths.recommendationState,
1253
+ JSON.stringify(state, null, 2)
1254
+ );
1255
+ }
1256
+ async function updateStatus(id, status, details) {
1257
+ const state = await loadState();
1258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1259
+ const existing = state.entries.find((e) => e.id === id);
1260
+ if (existing) {
1261
+ existing.status = status;
1262
+ existing.updated_at = now;
1263
+ if (status === "applied" && details !== void 0) {
1264
+ existing.applied_details = details;
1265
+ } else if (status !== "applied") {
1266
+ existing.applied_details = void 0;
1267
+ }
1268
+ } else {
1269
+ state.entries.push({
1270
+ id,
1271
+ status,
1272
+ updated_at: now,
1273
+ ...status === "applied" && details !== void 0 ? { applied_details: details } : {}
1274
+ });
1275
+ }
1276
+ state.last_updated = now;
1277
+ await saveState(state);
1278
+ }
1279
+ async function getStatusMap() {
1280
+ const state = await loadState();
1281
+ return new Map(state.entries.map((e) => [e.id, e.status]));
1282
+ }
1283
+ function isNodeError(err) {
1284
+ return err instanceof Error && "code" in err;
1285
+ }
1286
+
1287
+ // src/schemas/onboarding.ts
1288
+ import { z as z7 } from "zod/v4";
1289
+ var experienceTierSchema = z7.enum(["newcomer", "intermediate", "power_user"]);
1290
+ var experienceLevelSchema = z7.object({
1291
+ tier: experienceTierSchema,
1292
+ score: z7.number().min(0).max(100),
1293
+ breakdown: z7.object({
1294
+ hooks: z7.number(),
1295
+ rules: z7.number(),
1296
+ skills: z7.number(),
1297
+ plugins: z7.number(),
1298
+ claude_md: z7.number(),
1299
+ ecosystems: z7.number()
1300
+ })
1301
+ });
1302
+ var outcomeEntrySchema = z7.object({
1303
+ recommendation_id: z7.string(),
1304
+ pattern_type: z7.string(),
1305
+ target: z7.string(),
1306
+ applied_at: z7.iso.datetime(),
1307
+ checked_at: z7.iso.datetime(),
1308
+ persisted: z7.boolean(),
1309
+ checks_since_applied: z7.number(),
1310
+ outcome: z7.enum(["positive", "negative", "monitoring"])
1311
+ });
1312
+ var outcomeSummarySchema = z7.object({
1313
+ pattern_type: z7.string(),
1314
+ total_applied: z7.number(),
1315
+ total_persisted: z7.number(),
1316
+ total_reverted: z7.number(),
1317
+ persistence_rate: z7.number()
1318
+ });
1319
+
1320
+ // src/analysis/outcome-tracker.ts
1321
+ async function trackOutcomes(snapshot) {
1322
+ const state = await loadState();
1323
+ const applied = state.entries.filter((e) => e.status === "applied");
1324
+ if (applied.length === 0) return [];
1325
+ const history = await loadOutcomeHistory();
1326
+ const results = [];
1327
+ for (const entry of applied) {
1328
+ const priorEntries = history.filter(
1329
+ (h) => h.recommendation_id === entry.id
1330
+ );
1331
+ const latest = priorEntries.length > 0 ? priorEntries[priorEntries.length - 1] : void 0;
1332
+ const checksCount = latest ? latest.checks_since_applied + 1 : 1;
1333
+ const persisted = checkPersistence(entry, snapshot);
1334
+ let outcome;
1335
+ if (!persisted) {
1336
+ outcome = "negative";
1337
+ } else if (checksCount >= 5) {
1338
+ outcome = "positive";
1339
+ } else {
1340
+ outcome = "monitoring";
1341
+ }
1342
+ const patternType = inferPatternType(entry.id);
1343
+ const target = inferTarget(entry.id);
1344
+ const outcomeEntry = {
1345
+ recommendation_id: entry.id,
1346
+ pattern_type: patternType,
1347
+ target,
1348
+ applied_at: entry.updated_at,
1349
+ checked_at: (/* @__PURE__ */ new Date()).toISOString(),
1350
+ persisted,
1351
+ checks_since_applied: checksCount,
1352
+ outcome
1353
+ };
1354
+ results.push(outcomeEntry);
1355
+ await appendOutcome(outcomeEntry);
1356
+ }
1357
+ return results;
1358
+ }
1359
+ function checkPersistence(entry, snapshot) {
1360
+ if (entry.applied_details) {
1361
+ const toolMatch = entry.applied_details.match(/Added (\w+) to allowedTools/);
1362
+ if (toolMatch) {
1363
+ const toolName = toolMatch[1];
1364
+ const userSettings = snapshot.settings.user;
1365
+ if (!userSettings) return false;
1366
+ const allowedTools = userSettings.allowedTools;
1367
+ if (!Array.isArray(allowedTools)) return false;
1368
+ return allowedTools.includes(toolName);
1369
+ }
1370
+ }
1371
+ if (entry.id.startsWith("rec-repeated-")) {
1372
+ return snapshot.installed_tools.hooks.length > 0;
1373
+ }
1374
+ if (entry.id.startsWith("rec-long-")) {
1375
+ return snapshot.installed_tools.skills.length > 0;
1376
+ }
1377
+ if (entry.id.startsWith("rec-correction-")) {
1378
+ return snapshot.installed_tools.rules.length > 0;
1379
+ }
1380
+ return true;
1381
+ }
1382
+ async function appendOutcome(entry) {
1383
+ await appendFile(
1384
+ paths.outcomeHistory,
1385
+ JSON.stringify(entry) + "\n",
1386
+ "utf-8"
1387
+ );
1388
+ }
1389
+ async function loadOutcomeHistory() {
1390
+ let raw;
1391
+ try {
1392
+ raw = await readFile5(paths.outcomeHistory, "utf-8");
1393
+ } catch (err) {
1394
+ if (isNodeError2(err) && err.code === "ENOENT") {
1395
+ return [];
1396
+ }
1397
+ throw err;
1398
+ }
1399
+ const entries = [];
1400
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
1401
+ for (const line of lines) {
1402
+ try {
1403
+ const parsed = JSON.parse(line);
1404
+ const result = outcomeEntrySchema.safeParse(parsed);
1405
+ if (result.success) {
1406
+ entries.push(result.data);
1407
+ }
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ return entries;
1412
+ }
1413
+ function computeOutcomeSummaries(history) {
1414
+ if (history.length === 0) return [];
1415
+ const groups = /* @__PURE__ */ new Map();
1416
+ for (const entry of history) {
1417
+ const group = groups.get(entry.pattern_type) ?? [];
1418
+ group.push(entry);
1419
+ groups.set(entry.pattern_type, group);
1420
+ }
1421
+ const summaries = [];
1422
+ for (const [patternType, entries] of groups) {
1423
+ const latestByRec = /* @__PURE__ */ new Map();
1424
+ for (const entry of entries) {
1425
+ latestByRec.set(entry.recommendation_id, entry);
1426
+ }
1427
+ let totalPersisted = 0;
1428
+ let totalReverted = 0;
1429
+ for (const entry of latestByRec.values()) {
1430
+ if (entry.outcome === "positive") {
1431
+ totalPersisted++;
1432
+ } else if (entry.outcome === "negative") {
1433
+ totalReverted++;
1434
+ }
1435
+ }
1436
+ const totalApplied = latestByRec.size;
1437
+ const persistenceRate = totalApplied > 0 ? totalPersisted / totalApplied : 0;
1438
+ summaries.push({
1439
+ pattern_type: patternType,
1440
+ total_applied: totalApplied,
1441
+ total_persisted: totalPersisted,
1442
+ total_reverted: totalReverted,
1443
+ persistence_rate: persistenceRate
1444
+ });
1445
+ }
1446
+ return summaries;
1447
+ }
1448
+ function inferPatternType(id) {
1449
+ if (id.startsWith("rec-repeated-")) return "repeated_prompt";
1450
+ if (id.startsWith("rec-long-")) return "long_prompt";
1451
+ if (id.startsWith("rec-permission-always-approved-")) return "permission-always-approved";
1452
+ if (id.startsWith("rec-correction-")) return "code_correction";
1453
+ if (id.startsWith("rec-personal-")) return "personal_info";
1454
+ if (id.startsWith("rec-drift-")) return "config_drift";
1455
+ if (id.startsWith("rec-ecosystem-")) return "version_update";
1456
+ if (id.startsWith("rec-onboarding-")) return "onboarding_start_hooks";
1457
+ if (id.startsWith("rec-tool-preference-")) return "tool-preference";
1458
+ return "unknown";
1459
+ }
1460
+ function inferTarget(id) {
1461
+ if (id.startsWith("rec-repeated-")) return "HOOK";
1462
+ if (id.startsWith("rec-long-")) return "SKILL";
1463
+ if (id.startsWith("rec-permission-always-approved-")) return "SETTINGS";
1464
+ if (id.startsWith("rec-correction-")) return "RULE";
1465
+ if (id.startsWith("rec-personal-")) return "MEMORY";
1466
+ if (id.startsWith("rec-drift-")) return "CLAUDE_MD";
1467
+ if (id.startsWith("rec-ecosystem-")) return "CLAUDE_MD";
1468
+ if (id.startsWith("rec-tool-preference-")) return "SETTINGS";
1469
+ if (id.startsWith("rec-onboarding-")) return "HOOK";
1470
+ return "MEMORY";
1471
+ }
1472
+ function isNodeError2(err) {
1473
+ return err instanceof Error && "code" in err;
1474
+ }
1475
+
1476
+ // src/analysis/trigger.ts
1477
+ import writeFileAtomic6 from "write-file-atomic";
1478
+ import { readFile as readFile6 } from "fs/promises";
1479
+ import { lock as lock2 } from "proper-lockfile";
1480
+ async function writeAnalysisResult(result) {
1481
+ await ensureInit();
1482
+ await writeFileAtomic6(paths.analysisResult, JSON.stringify(result, null, 2));
1483
+ }
1484
+ async function runAnalysis(cwd) {
1485
+ const summary = await preProcess();
1486
+ const snapshot = await scanEnvironment(cwd);
1487
+ let outcomeSummaries;
1488
+ try {
1489
+ await trackOutcomes(snapshot);
1490
+ const history = await loadOutcomeHistory();
1491
+ outcomeSummaries = computeOutcomeSummaries(history);
1492
+ } catch {
1493
+ }
1494
+ const result = analyze(summary, snapshot, void 0, outcomeSummaries);
1495
+ await writeAnalysisResult(result);
1496
+ return result;
1497
+ }
1498
+
1499
+ // src/delivery/renderer.ts
1500
+ var TIER_ORDER = ["HIGH", "MEDIUM", "LOW"];
1501
+ function renderRecommendations(result, states) {
1502
+ const lines = [];
1503
+ lines.push("# harness-evolve Recommendations");
1504
+ lines.push("");
1505
+ lines.push(`*Generated: ${result.generated_at}*`);
1506
+ lines.push(
1507
+ `*Period: ${result.summary_period.since} to ${result.summary_period.until} (${result.summary_period.days} days)*`
1508
+ );
1509
+ lines.push("");
1510
+ if (result.recommendations.length === 0) {
1511
+ lines.push("No recommendations at this time.");
1512
+ lines.push("");
1513
+ lines.push("---");
1514
+ lines.push("*Run /evolve to refresh or manage recommendations.*");
1515
+ return lines.join("\n");
1516
+ }
1517
+ for (const tier of TIER_ORDER) {
1518
+ const tierRecs = result.recommendations.filter(
1519
+ (r) => r.confidence === tier
1520
+ );
1521
+ if (tierRecs.length === 0) continue;
1522
+ lines.push(`## ${tier} Confidence`);
1523
+ lines.push("");
1524
+ for (const rec of tierRecs) {
1525
+ const status = (states.get(rec.id) ?? "pending").toUpperCase();
1526
+ lines.push(`### [${status}] ${rec.title}`);
1527
+ lines.push("");
1528
+ lines.push(`**Target:** ${rec.target} | **Pattern:** ${rec.pattern_type}`);
1529
+ const evidenceParts = [`${rec.evidence.count} occurrences`];
1530
+ if (rec.evidence.sessions !== void 0) {
1531
+ evidenceParts.push(`across ${rec.evidence.sessions} sessions`);
1532
+ }
1533
+ lines.push(`**Evidence:** ${evidenceParts.join(" ")}`);
1534
+ lines.push("");
1535
+ lines.push(rec.description);
1536
+ lines.push("");
1537
+ if (rec.evidence.examples.length > 0) {
1538
+ lines.push("**Examples:**");
1539
+ for (const ex of rec.evidence.examples) {
1540
+ lines.push(`- \`${ex}\``);
1541
+ }
1542
+ lines.push("");
1543
+ }
1544
+ lines.push(`**Suggested action:** ${rec.suggested_action}`);
1545
+ lines.push("");
1546
+ if (rec.ecosystem_context !== void 0) {
1547
+ lines.push(`**Ecosystem note:** ${rec.ecosystem_context}`);
1548
+ lines.push("");
1549
+ }
1550
+ }
1551
+ }
1552
+ lines.push("---");
1553
+ lines.push("*Run /evolve to refresh or manage recommendations.*");
1554
+ return lines.join("\n");
1555
+ }
1556
+
1557
+ // src/delivery/rotator.ts
1558
+ import { mkdir as mkdir2 } from "fs/promises";
1559
+ import { join as join4 } from "path";
1560
+ import writeFileAtomic7 from "write-file-atomic";
1561
+ async function rotateRecommendations(config) {
1562
+ const state = await loadState();
1563
+ const cutoff = new Date(Date.now() - config.archiveAfterDays * 864e5);
1564
+ const toArchive = state.entries.filter(
1565
+ (e) => (e.status === "applied" || e.status === "dismissed") && new Date(e.updated_at) < cutoff
1566
+ );
1567
+ if (toArchive.length === 0) {
1568
+ return;
1569
+ }
1570
+ const toKeep = state.entries.filter(
1571
+ (e) => !toArchive.some((a) => a.id === e.id)
1572
+ );
1573
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1574
+ const archivePath = join4(paths.recommendationArchive, `${today}.json`);
1575
+ await mkdir2(paths.recommendationArchive, { recursive: true });
1576
+ await writeFileAtomic7(archivePath, JSON.stringify(toArchive, null, 2));
1577
+ await saveState({
1578
+ entries: toKeep,
1579
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
1580
+ });
1581
+ }
1582
+
1583
+ // src/delivery/notification.ts
1584
+ import { existsSync } from "fs";
1585
+ import { readFile as readFile7, unlink, writeFile } from "fs/promises";
1586
+ async function writeNotificationFlag(pendingCount) {
1587
+ await writeFile(paths.notificationFlag, String(pendingCount), "utf-8");
1588
+ }
1589
+
1590
+ // src/delivery/auto-apply.ts
1591
+ import { appendFile as appendFile2 } from "fs/promises";
1592
+
1593
+ // src/delivery/appliers/index.ts
1594
+ var registry = /* @__PURE__ */ new Map();
1595
+ function registerApplier(applier) {
1596
+ registry.set(applier.target, applier);
1597
+ }
1598
+ function getApplier(target) {
1599
+ return registry.get(target);
1600
+ }
1601
+ function hasApplier(target) {
1602
+ return registry.has(target);
1603
+ }
1604
+
1605
+ // src/delivery/appliers/settings-applier.ts
1606
+ import { readFile as readFile8, copyFile, mkdir as mkdir3 } from "fs/promises";
1607
+ import { join as join5, dirname } from "path";
1608
+ import writeFileAtomic8 from "write-file-atomic";
1609
+ var SettingsApplier = class {
1610
+ target = "SETTINGS";
1611
+ canApply(rec) {
1612
+ return rec.confidence === "HIGH" && rec.target === "SETTINGS" && rec.pattern_type === "permission-always-approved";
1613
+ }
1614
+ async apply(rec, options) {
1615
+ try {
1616
+ if (rec.pattern_type !== "permission-always-approved") {
1617
+ return {
1618
+ recommendation_id: rec.id,
1619
+ success: false,
1620
+ details: `Skipped: pattern_type '${rec.pattern_type}' not supported for auto-apply in v1`
1621
+ };
1622
+ }
1623
+ const settingsFilePath = options?.settingsPath ?? join5(process.env.HOME ?? "", ".claude", "settings.json");
1624
+ const raw = await readFile8(settingsFilePath, "utf-8");
1625
+ const settings = JSON.parse(raw);
1626
+ const backup = join5(
1627
+ paths.analysis,
1628
+ "backups",
1629
+ `settings-backup-${rec.id}.json`
1630
+ );
1631
+ await mkdir3(dirname(backup), { recursive: true });
1632
+ await copyFile(settingsFilePath, backup);
1633
+ const toolName = extractToolName(rec);
1634
+ if (!toolName) {
1635
+ return {
1636
+ recommendation_id: rec.id,
1637
+ success: false,
1638
+ details: "Could not extract tool name from recommendation evidence"
1639
+ };
1640
+ }
1641
+ const allowedTools = Array.isArray(settings.allowedTools) ? settings.allowedTools : [];
1642
+ if (!allowedTools.includes(toolName)) {
1643
+ allowedTools.push(toolName);
1644
+ }
1645
+ settings.allowedTools = allowedTools;
1646
+ await writeFileAtomic8(
1647
+ settingsFilePath,
1648
+ JSON.stringify(settings, null, 2)
1649
+ );
1650
+ return {
1651
+ recommendation_id: rec.id,
1652
+ success: true,
1653
+ details: `Added ${toolName} to allowedTools`
1654
+ };
1655
+ } catch (err) {
1656
+ const message = err instanceof Error ? err.message : String(err);
1657
+ return {
1658
+ recommendation_id: rec.id,
1659
+ success: false,
1660
+ details: message
1661
+ };
1662
+ }
1663
+ }
1664
+ };
1665
+ function extractToolName(rec) {
1666
+ for (const example of rec.evidence.examples) {
1667
+ const match = example.match(/^(\w+)\(/);
1668
+ if (match) return match[1];
1669
+ }
1670
+ return void 0;
1671
+ }
1672
+
1673
+ // src/delivery/appliers/rule-applier.ts
1674
+ import { writeFile as writeFile2, access as access2, mkdir as mkdir4 } from "fs/promises";
1675
+ import { join as join6 } from "path";
1676
+ var RuleApplier = class {
1677
+ target = "RULE";
1678
+ canApply(rec) {
1679
+ return rec.confidence === "HIGH" && rec.target === "RULE";
1680
+ }
1681
+ async apply(rec, options) {
1682
+ const rulesDir = options?.rulesDir ?? join6(process.env.HOME ?? "", ".claude", "rules");
1683
+ const fileName = `evolve-${rec.pattern_type}.md`;
1684
+ const filePath = join6(rulesDir, fileName);
1685
+ try {
1686
+ await access2(filePath);
1687
+ return {
1688
+ recommendation_id: rec.id,
1689
+ success: false,
1690
+ details: `Rule file already exists: ${fileName}`
1691
+ };
1692
+ } catch {
1693
+ }
1694
+ try {
1695
+ await mkdir4(rulesDir, { recursive: true });
1696
+ const content = [
1697
+ `# ${rec.title}`,
1698
+ "",
1699
+ rec.description,
1700
+ "",
1701
+ "## Action",
1702
+ "",
1703
+ rec.suggested_action,
1704
+ "",
1705
+ "---",
1706
+ `*Auto-generated by harness-evolve (${rec.id})*`
1707
+ ].join("\n");
1708
+ await writeFile2(filePath, content, "utf-8");
1709
+ return {
1710
+ recommendation_id: rec.id,
1711
+ success: true,
1712
+ details: `Created rule file: ${fileName}`
1713
+ };
1714
+ } catch (err) {
1715
+ const message = err instanceof Error ? err.message : String(err);
1716
+ return {
1717
+ recommendation_id: rec.id,
1718
+ success: false,
1719
+ details: message
1720
+ };
1721
+ }
1722
+ }
1723
+ };
1724
+
1725
+ // src/delivery/appliers/hook-applier.ts
1726
+ import { writeFile as writeFile3, access as access3, mkdir as mkdir5, chmod, copyFile as copyFile2 } from "fs/promises";
1727
+ import { join as join8, basename } from "path";
1728
+
1729
+ // src/generators/schemas.ts
1730
+ import { z as z8 } from "zod/v4";
1731
+ var GENERATOR_VERSION = "1.0.0";
1732
+ function nowISO() {
1733
+ return (/* @__PURE__ */ new Date()).toISOString();
1734
+ }
1735
+ function toSlug(text) {
1736
+ if (!text) return "";
1737
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1738
+ }
1739
+ var generatedArtifactSchema = z8.object({
1740
+ type: z8.enum(["skill", "hook", "claude_md_patch"]),
1741
+ filename: z8.string(),
1742
+ content: z8.string(),
1743
+ source_recommendation_id: z8.string(),
1744
+ metadata: z8.object({
1745
+ generated_at: z8.iso.datetime(),
1746
+ generator_version: z8.string(),
1747
+ pattern_type: z8.string()
1748
+ })
1749
+ });
1750
+
1751
+ // src/generators/hook-generator.ts
1752
+ function extractHookEvent(rec) {
1753
+ const descMatch = rec.description.match(/suitable for a (\w+) hook/i);
1754
+ if (descMatch) return descMatch[1];
1755
+ const actionMatch = rec.suggested_action.match(/Create a (\w+) hook/i);
1756
+ if (actionMatch) return actionMatch[1];
1757
+ return "PreToolUse";
1758
+ }
1759
+ function generateHook(rec) {
1760
+ if (rec.target !== "HOOK") return null;
1761
+ const hookEvent = extractHookEvent(rec);
1762
+ const slugName = toSlug(rec.title);
1763
+ const content = [
1764
+ "#!/usr/bin/env bash",
1765
+ `# Auto-generated hook for: ${rec.title}`,
1766
+ `# Hook event: ${hookEvent}`,
1767
+ `# Source: harness-evolve (${rec.id})`,
1768
+ "#",
1769
+ "# TODO: Review and customize this script before use.",
1770
+ "",
1771
+ "# Read hook input from stdin",
1772
+ "INPUT=$(cat)",
1773
+ "",
1774
+ "# Extract relevant fields",
1775
+ `# Adjust jq path based on your ${hookEvent} event schema`,
1776
+ "",
1777
+ `# ${rec.suggested_action}`,
1778
+ "",
1779
+ "# Exit 0 to allow, exit 2 to block",
1780
+ "exit 0"
1781
+ ].join("\n");
1782
+ return {
1783
+ type: "hook",
1784
+ filename: `.claude/hooks/evolve-${slugName}.sh`,
1785
+ content,
1786
+ source_recommendation_id: rec.id,
1787
+ metadata: {
1788
+ generated_at: nowISO(),
1789
+ generator_version: GENERATOR_VERSION,
1790
+ pattern_type: rec.pattern_type
1791
+ }
1792
+ };
1793
+ }
1794
+
1795
+ // src/cli/utils.ts
1796
+ import { readFile as readFile9 } from "fs/promises";
1797
+ import { join as join7 } from "path";
1798
+ import { createInterface as createInterface2 } from "readline/promises";
1799
+ import writeFileAtomic9 from "write-file-atomic";
1800
+ var HARNESS_EVOLVE_MARKER = "harness-evolve";
1801
+ var SETTINGS_PATH = join7(
1802
+ process.env.HOME ?? "",
1803
+ ".claude",
1804
+ "settings.json"
1805
+ );
1806
+ async function readSettings(settingsPath) {
1807
+ const filePath = settingsPath ?? SETTINGS_PATH;
1808
+ try {
1809
+ const raw = await readFile9(filePath, "utf-8");
1810
+ return JSON.parse(raw);
1811
+ } catch {
1812
+ return {};
1813
+ }
1814
+ }
1815
+ async function writeSettings(settings, settingsPath) {
1816
+ const filePath = settingsPath ?? SETTINGS_PATH;
1817
+ await writeFileAtomic9(filePath, JSON.stringify(settings, null, 2));
1818
+ }
1819
+ function mergeHooks(existing, hookCommands) {
1820
+ const hooks = existing.hooks != null ? { ...existing.hooks } : {};
1821
+ for (const hc of hookCommands) {
1822
+ const eventArray = Array.isArray(hooks[hc.event]) ? [...hooks[hc.event]] : [];
1823
+ const alreadyRegistered = eventArray.some((entry) => {
1824
+ const innerHooks = entry.hooks;
1825
+ if (!Array.isArray(innerHooks)) return false;
1826
+ return innerHooks.some(
1827
+ (h) => String(h.command ?? "").includes(HARNESS_EVOLVE_MARKER)
1828
+ );
1829
+ });
1830
+ if (!alreadyRegistered) {
1831
+ const hookEntry = {
1832
+ type: "command",
1833
+ command: hc.command,
1834
+ timeout: hc.timeout
1835
+ };
1836
+ if (hc.async) {
1837
+ hookEntry.async = true;
1838
+ }
1839
+ eventArray.push({
1840
+ matcher: "*",
1841
+ hooks: [hookEntry]
1842
+ });
1843
+ }
1844
+ hooks[hc.event] = eventArray;
1845
+ }
1846
+ return { ...existing, hooks };
1847
+ }
1848
+
1849
+ // src/delivery/appliers/hook-applier.ts
1850
+ var HookApplier = class {
1851
+ target = "HOOK";
1852
+ canApply(rec) {
1853
+ return rec.confidence === "HIGH" && rec.target === "HOOK";
1854
+ }
1855
+ async apply(rec, options) {
1856
+ try {
1857
+ const artifact = generateHook(rec);
1858
+ if (!artifact) {
1859
+ return {
1860
+ recommendation_id: rec.id,
1861
+ success: false,
1862
+ details: "Generator returned null \u2014 recommendation not applicable for hook generation"
1863
+ };
1864
+ }
1865
+ const hooksDir = options?.hooksDir ?? join8(process.env.HOME ?? "", ".claude", "hooks");
1866
+ const scriptFilename = basename(artifact.filename);
1867
+ const scriptPath = join8(hooksDir, scriptFilename);
1868
+ try {
1869
+ await access3(scriptPath);
1870
+ return {
1871
+ recommendation_id: rec.id,
1872
+ success: false,
1873
+ details: `Hook file already exists: ${scriptFilename}`
1874
+ };
1875
+ } catch {
1876
+ }
1877
+ await mkdir5(hooksDir, { recursive: true });
1878
+ await writeFile3(scriptPath, artifact.content, "utf-8");
1879
+ await chmod(scriptPath, 493);
1880
+ const settingsPath = options?.settingsPath ?? join8(process.env.HOME ?? "", ".claude", "settings.json");
1881
+ const settings = await readSettings(settingsPath);
1882
+ const backupDir = join8(paths.analysis, "backups");
1883
+ await mkdir5(backupDir, { recursive: true });
1884
+ const backupFile = join8(backupDir, `settings-backup-${rec.id}.json`);
1885
+ try {
1886
+ await copyFile2(settingsPath, backupFile);
1887
+ } catch {
1888
+ await writeFile3(backupFile, JSON.stringify(settings, null, 2), "utf-8");
1889
+ }
1890
+ const eventMatch = artifact.content.match(/# Hook event: (\w+)/);
1891
+ const hookEvent = eventMatch?.[1] ?? "PreToolUse";
1892
+ const merged = mergeHooks(settings, [
1893
+ {
1894
+ event: hookEvent,
1895
+ command: `bash "${scriptPath}"`,
1896
+ timeout: 10,
1897
+ async: true
1898
+ }
1899
+ ]);
1900
+ await writeSettings(merged, settingsPath);
1901
+ return {
1902
+ recommendation_id: rec.id,
1903
+ success: true,
1904
+ details: `Created hook script: ${scriptFilename} and registered under ${hookEvent}`
1905
+ };
1906
+ } catch (err) {
1907
+ const message = err instanceof Error ? err.message : String(err);
1908
+ return {
1909
+ recommendation_id: rec.id,
1910
+ success: false,
1911
+ details: message
1912
+ };
1913
+ }
1914
+ }
1915
+ };
1916
+
1917
+ // src/delivery/appliers/claude-md-applier.ts
1918
+ import { readFile as readFile10, mkdir as mkdir6 } from "fs/promises";
1919
+ import { join as join9, dirname as dirname4 } from "path";
1920
+ import writeFileAtomic10 from "write-file-atomic";
1921
+ var DESTRUCTIVE_PATTERNS = /* @__PURE__ */ new Set([
1922
+ "scan_stale_reference",
1923
+ "scan_redundancy"
1924
+ ]);
1925
+ var ClaudeMdApplier = class {
1926
+ target = "CLAUDE_MD";
1927
+ canApply(rec) {
1928
+ return rec.confidence === "HIGH" && rec.target === "CLAUDE_MD";
1929
+ }
1930
+ async apply(rec, options) {
1931
+ try {
1932
+ if (DESTRUCTIVE_PATTERNS.has(rec.pattern_type)) {
1933
+ return {
1934
+ recommendation_id: rec.id,
1935
+ success: false,
1936
+ details: `Pattern type '${rec.pattern_type}' requires manual review \u2014 cannot safely auto-apply`
1937
+ };
1938
+ }
1939
+ const claudeMdPath = options?.claudeMdPath ?? join9(process.cwd(), "CLAUDE.md");
1940
+ let existingContent = "";
1941
+ try {
1942
+ existingContent = await readFile10(claudeMdPath, "utf-8");
1943
+ } catch {
1944
+ }
1945
+ if (existingContent) {
1946
+ const backupDir = join9(paths.analysis, "backups");
1947
+ await mkdir6(backupDir, { recursive: true });
1948
+ const backupFile = join9(backupDir, `claudemd-backup-${rec.id}.md`);
1949
+ await writeFileAtomic10(backupFile, existingContent);
1950
+ }
1951
+ const newSection = [
1952
+ "",
1953
+ "",
1954
+ `## ${rec.title}`,
1955
+ "",
1956
+ rec.suggested_action,
1957
+ "",
1958
+ "---",
1959
+ `*Auto-generated by harness-evolve (${rec.id})*`,
1960
+ ""
1961
+ ].join("\n");
1962
+ const updatedContent = existingContent + newSection;
1963
+ await mkdir6(dirname4(claudeMdPath), { recursive: true });
1964
+ await writeFileAtomic10(claudeMdPath, updatedContent);
1965
+ return {
1966
+ recommendation_id: rec.id,
1967
+ success: true,
1968
+ details: `Appended section: ${rec.title}`
1969
+ };
1970
+ } catch (err) {
1971
+ const message = err instanceof Error ? err.message : String(err);
1972
+ return {
1973
+ recommendation_id: rec.id,
1974
+ success: false,
1975
+ details: message
1976
+ };
1977
+ }
1978
+ }
1979
+ };
1980
+
1981
+ // src/delivery/auto-apply.ts
1982
+ registerApplier(new SettingsApplier());
1983
+ registerApplier(new RuleApplier());
1984
+ registerApplier(new HookApplier());
1985
+ registerApplier(new ClaudeMdApplier());
1986
+ async function autoApplyRecommendations(recommendations, options) {
1987
+ const config = await loadConfig();
1988
+ if (!config.delivery.fullAuto) return [];
1989
+ await ensureInit();
1990
+ const stateMap = await getStatusMap();
1991
+ const results = [];
1992
+ const candidates = recommendations.filter(
1993
+ (rec) => rec.confidence === "HIGH" && hasApplier(rec.target) && (stateMap.get(rec.id) ?? "pending") === "pending"
1994
+ );
1995
+ for (const rec of candidates) {
1996
+ const applier = getApplier(rec.target);
1997
+ let result;
1998
+ if (!applier || !applier.canApply(rec)) {
1999
+ result = {
2000
+ recommendation_id: rec.id,
2001
+ success: false,
2002
+ details: `No applicable applier for target '${rec.target}' with pattern_type '${rec.pattern_type}'`
2003
+ };
2004
+ } else {
2005
+ result = await applier.apply(rec, options);
2006
+ }
2007
+ results.push(result);
2008
+ const logEntry = {
2009
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2010
+ recommendation_id: rec.id,
2011
+ target: rec.target,
2012
+ action: rec.suggested_action,
2013
+ success: result.success,
2014
+ details: result.details,
2015
+ backup_path: void 0
2016
+ };
2017
+ await appendFile2(
2018
+ paths.autoApplyLog,
2019
+ JSON.stringify(logEntry) + "\n",
2020
+ "utf-8"
2021
+ );
2022
+ if (result.success) {
2023
+ await updateStatus(rec.id, "applied", `Auto-applied: ${result.details}`);
2024
+ }
2025
+ }
2026
+ return results;
2027
+ }
2028
+
2029
+ // src/delivery/run-evolve.ts
2030
+ import writeFileAtomic11 from "write-file-atomic";
2031
+ async function main() {
2032
+ const cwd = process.argv[2] || process.cwd();
2033
+ await ensureInit();
2034
+ const config = await loadConfig();
2035
+ const result = await runAnalysis(cwd);
2036
+ const stateMap = await getStatusMap();
2037
+ await rotateRecommendations({
2038
+ maxRecommendationsInFile: config.delivery.maxRecommendationsInFile,
2039
+ archiveAfterDays: config.delivery.archiveAfterDays
2040
+ });
2041
+ const markdown = renderRecommendations(result, stateMap);
2042
+ await writeFileAtomic11(paths.recommendations, markdown);
2043
+ try {
2044
+ await autoApplyRecommendations(result.recommendations);
2045
+ } catch {
2046
+ }
2047
+ const updatedStateMap = await getStatusMap();
2048
+ const pendingCount = result.recommendations.filter(
2049
+ (r) => (updatedStateMap.get(r.id) ?? "pending") === "pending"
2050
+ ).length;
2051
+ if (pendingCount > 0) {
2052
+ await writeNotificationFlag(pendingCount);
2053
+ }
2054
+ const pending = result.recommendations.filter(
2055
+ (r) => (updatedStateMap.get(r.id) ?? "pending") === "pending"
2056
+ );
2057
+ console.log(
2058
+ JSON.stringify({
2059
+ total: result.recommendations.length,
2060
+ pending: pending.length,
2061
+ high: pending.filter((r) => r.confidence === "HIGH").length,
2062
+ medium: pending.filter((r) => r.confidence === "MEDIUM").length,
2063
+ low: pending.filter((r) => r.confidence === "LOW").length,
2064
+ file: paths.recommendations
2065
+ })
2066
+ );
2067
+ }
2068
+ main().catch(() => process.exit(1));
2069
+ //# sourceMappingURL=run-evolve.js.map