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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/cli.js +1685 -0
- package/dist/cli.js.map +1 -0
- package/dist/delivery/run-evolve.d.ts +2 -0
- package/dist/delivery/run-evolve.js +2069 -0
- package/dist/delivery/run-evolve.js.map +1 -0
- package/dist/hooks/permission-request.d.ts +8 -0
- package/dist/hooks/permission-request.js +405 -0
- package/dist/hooks/permission-request.js.map +1 -0
- package/dist/hooks/post-tool-use-failure.d.ts +9 -0
- package/dist/hooks/post-tool-use-failure.js +437 -0
- package/dist/hooks/post-tool-use-failure.js.map +1 -0
- package/dist/hooks/post-tool-use.d.ts +9 -0
- package/dist/hooks/post-tool-use.js +441 -0
- package/dist/hooks/post-tool-use.js.map +1 -0
- package/dist/hooks/pre-tool-use.d.ts +8 -0
- package/dist/hooks/pre-tool-use.js +434 -0
- package/dist/hooks/pre-tool-use.js.map +1 -0
- package/dist/hooks/stop.d.ts +8 -0
- package/dist/hooks/stop.js +1609 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/hooks/user-prompt-submit.d.ts +8 -0
- package/dist/hooks/user-prompt-submit.js +442 -0
- package/dist/hooks/user-prompt-submit.js.map +1 -0
- package/dist/index.d.ts +1029 -0
- package/dist/index.js +3131 -0
- package/dist/index.js.map +1 -0
- package/package.json +104 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3131 @@
|
|
|
1
|
+
// src/schemas/config.ts
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
var configSchema = z.object({
|
|
4
|
+
version: z.number().default(1),
|
|
5
|
+
analysis: z.object({
|
|
6
|
+
threshold: z.number().min(1).default(50),
|
|
7
|
+
enabled: z.boolean().default(true),
|
|
8
|
+
classifierThresholds: z.record(z.string(), z.number()).default({})
|
|
9
|
+
}).default({ threshold: 50, enabled: true, classifierThresholds: {} }),
|
|
10
|
+
hooks: z.object({
|
|
11
|
+
capturePrompts: z.boolean().default(true),
|
|
12
|
+
captureTools: z.boolean().default(true),
|
|
13
|
+
capturePermissions: z.boolean().default(true),
|
|
14
|
+
captureSessions: z.boolean().default(true)
|
|
15
|
+
}).default({
|
|
16
|
+
capturePrompts: true,
|
|
17
|
+
captureTools: true,
|
|
18
|
+
capturePermissions: true,
|
|
19
|
+
captureSessions: true
|
|
20
|
+
}),
|
|
21
|
+
scrubbing: z.object({
|
|
22
|
+
enabled: z.boolean().default(true),
|
|
23
|
+
highEntropyDetection: z.boolean().default(false),
|
|
24
|
+
customPatterns: z.array(z.object({
|
|
25
|
+
name: z.string(),
|
|
26
|
+
regex: z.string(),
|
|
27
|
+
replacement: z.string()
|
|
28
|
+
})).default([])
|
|
29
|
+
}).default({
|
|
30
|
+
enabled: true,
|
|
31
|
+
highEntropyDetection: false,
|
|
32
|
+
customPatterns: []
|
|
33
|
+
}),
|
|
34
|
+
delivery: z.object({
|
|
35
|
+
stdoutInjection: z.boolean().default(true),
|
|
36
|
+
maxTokens: z.number().default(200),
|
|
37
|
+
fullAuto: z.boolean().default(false),
|
|
38
|
+
maxRecommendationsInFile: z.number().default(20),
|
|
39
|
+
archiveAfterDays: z.number().default(7)
|
|
40
|
+
}).default({
|
|
41
|
+
stdoutInjection: true,
|
|
42
|
+
maxTokens: 200,
|
|
43
|
+
fullAuto: false,
|
|
44
|
+
maxRecommendationsInFile: 20,
|
|
45
|
+
archiveAfterDays: 7
|
|
46
|
+
})
|
|
47
|
+
}).strict();
|
|
48
|
+
|
|
49
|
+
// src/schemas/log-entry.ts
|
|
50
|
+
import { z as z2 } from "zod/v4";
|
|
51
|
+
var promptEntrySchema = z2.object({
|
|
52
|
+
timestamp: z2.iso.datetime(),
|
|
53
|
+
session_id: z2.string(),
|
|
54
|
+
cwd: z2.string(),
|
|
55
|
+
prompt: z2.string(),
|
|
56
|
+
prompt_length: z2.number(),
|
|
57
|
+
transcript_path: z2.string().optional()
|
|
58
|
+
});
|
|
59
|
+
var toolEntrySchema = z2.object({
|
|
60
|
+
timestamp: z2.iso.datetime(),
|
|
61
|
+
session_id: z2.string(),
|
|
62
|
+
event: z2.enum(["pre", "post", "failure"]),
|
|
63
|
+
tool_name: z2.string(),
|
|
64
|
+
input_summary: z2.string().optional(),
|
|
65
|
+
duration_ms: z2.number().optional(),
|
|
66
|
+
success: z2.boolean().optional()
|
|
67
|
+
});
|
|
68
|
+
var permissionEntrySchema = z2.object({
|
|
69
|
+
timestamp: z2.iso.datetime(),
|
|
70
|
+
session_id: z2.string(),
|
|
71
|
+
tool_name: z2.string(),
|
|
72
|
+
decision: z2.enum(["approved", "denied", "unknown"])
|
|
73
|
+
});
|
|
74
|
+
var sessionEntrySchema = z2.object({
|
|
75
|
+
timestamp: z2.iso.datetime(),
|
|
76
|
+
session_id: z2.string(),
|
|
77
|
+
event: z2.enum(["start", "end"]),
|
|
78
|
+
cwd: z2.string().optional()
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/schemas/counter.ts
|
|
82
|
+
import { z as z3 } from "zod/v4";
|
|
83
|
+
var counterSchema = z3.object({
|
|
84
|
+
total: z3.number().default(0),
|
|
85
|
+
session: z3.record(z3.string(), z3.number()).default({}),
|
|
86
|
+
last_analysis: z3.iso.datetime().optional(),
|
|
87
|
+
last_updated: z3.iso.datetime()
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// src/schemas/hook-input.ts
|
|
91
|
+
import { z as z4 } from "zod/v4";
|
|
92
|
+
var hookCommonSchema = z4.object({
|
|
93
|
+
session_id: z4.string(),
|
|
94
|
+
transcript_path: z4.string(),
|
|
95
|
+
cwd: z4.string(),
|
|
96
|
+
permission_mode: z4.string()
|
|
97
|
+
});
|
|
98
|
+
var userPromptSubmitInputSchema = hookCommonSchema.extend({
|
|
99
|
+
hook_event_name: z4.literal("UserPromptSubmit"),
|
|
100
|
+
prompt: z4.string()
|
|
101
|
+
});
|
|
102
|
+
var preToolUseInputSchema = hookCommonSchema.extend({
|
|
103
|
+
hook_event_name: z4.literal("PreToolUse"),
|
|
104
|
+
tool_name: z4.string(),
|
|
105
|
+
tool_input: z4.record(z4.string(), z4.unknown()),
|
|
106
|
+
tool_use_id: z4.string()
|
|
107
|
+
});
|
|
108
|
+
var postToolUseInputSchema = hookCommonSchema.extend({
|
|
109
|
+
hook_event_name: z4.literal("PostToolUse"),
|
|
110
|
+
tool_name: z4.string(),
|
|
111
|
+
tool_input: z4.record(z4.string(), z4.unknown()),
|
|
112
|
+
tool_response: z4.unknown().optional(),
|
|
113
|
+
tool_use_id: z4.string()
|
|
114
|
+
});
|
|
115
|
+
var postToolUseFailureInputSchema = hookCommonSchema.extend({
|
|
116
|
+
hook_event_name: z4.literal("PostToolUseFailure"),
|
|
117
|
+
tool_name: z4.string(),
|
|
118
|
+
tool_input: z4.record(z4.string(), z4.unknown()),
|
|
119
|
+
tool_use_id: z4.string(),
|
|
120
|
+
error: z4.string().optional(),
|
|
121
|
+
is_interrupt: z4.boolean().optional()
|
|
122
|
+
});
|
|
123
|
+
var permissionRequestInputSchema = hookCommonSchema.extend({
|
|
124
|
+
hook_event_name: z4.literal("PermissionRequest"),
|
|
125
|
+
tool_name: z4.string(),
|
|
126
|
+
tool_input: z4.record(z4.string(), z4.unknown()),
|
|
127
|
+
permission_suggestions: z4.array(z4.unknown()).optional()
|
|
128
|
+
});
|
|
129
|
+
var stopInputSchema = hookCommonSchema.extend({
|
|
130
|
+
hook_event_name: z4.literal("Stop"),
|
|
131
|
+
stop_hook_active: z4.boolean(),
|
|
132
|
+
last_assistant_message: z4.string().optional()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/hooks/shared.ts
|
|
136
|
+
var MAX_LEN = 200;
|
|
137
|
+
function truncate(str, maxLen) {
|
|
138
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
139
|
+
}
|
|
140
|
+
function readFromStream(stream) {
|
|
141
|
+
return new Promise((resolve2, reject) => {
|
|
142
|
+
let data = "";
|
|
143
|
+
stream.setEncoding("utf-8");
|
|
144
|
+
stream.on("data", (chunk) => {
|
|
145
|
+
data += chunk;
|
|
146
|
+
});
|
|
147
|
+
stream.on("end", () => resolve2(data));
|
|
148
|
+
stream.on("error", reject);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function readStdin() {
|
|
152
|
+
return readFromStream(process.stdin);
|
|
153
|
+
}
|
|
154
|
+
function summarizeToolInput(toolName, toolInput) {
|
|
155
|
+
switch (toolName) {
|
|
156
|
+
case "Bash":
|
|
157
|
+
return truncate(String(toolInput.command ?? ""), MAX_LEN);
|
|
158
|
+
case "Write":
|
|
159
|
+
case "Edit":
|
|
160
|
+
case "Read":
|
|
161
|
+
return truncate(String(toolInput.file_path ?? ""), MAX_LEN);
|
|
162
|
+
case "Glob":
|
|
163
|
+
return truncate(String(toolInput.pattern ?? ""), MAX_LEN);
|
|
164
|
+
case "Grep":
|
|
165
|
+
return truncate(String(toolInput.pattern ?? ""), MAX_LEN);
|
|
166
|
+
default: {
|
|
167
|
+
const str = JSON.stringify(toolInput);
|
|
168
|
+
return truncate(str, MAX_LEN);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/storage/counter.ts
|
|
174
|
+
import { readFile } from "fs/promises";
|
|
175
|
+
import { lock } from "proper-lockfile";
|
|
176
|
+
import writeFileAtomic from "write-file-atomic";
|
|
177
|
+
|
|
178
|
+
// src/storage/dirs.ts
|
|
179
|
+
import { mkdir } from "fs/promises";
|
|
180
|
+
import { join } from "path";
|
|
181
|
+
var BASE_DIR = join(process.env.HOME ?? "", ".harness-evolve");
|
|
182
|
+
var paths = {
|
|
183
|
+
base: BASE_DIR,
|
|
184
|
+
logs: {
|
|
185
|
+
prompts: join(BASE_DIR, "logs", "prompts"),
|
|
186
|
+
tools: join(BASE_DIR, "logs", "tools"),
|
|
187
|
+
permissions: join(BASE_DIR, "logs", "permissions"),
|
|
188
|
+
sessions: join(BASE_DIR, "logs", "sessions")
|
|
189
|
+
},
|
|
190
|
+
analysis: join(BASE_DIR, "analysis"),
|
|
191
|
+
analysisPreProcessed: join(BASE_DIR, "analysis", "pre-processed"),
|
|
192
|
+
summary: join(BASE_DIR, "analysis", "pre-processed", "summary.json"),
|
|
193
|
+
environmentSnapshot: join(BASE_DIR, "analysis", "environment-snapshot.json"),
|
|
194
|
+
analysisResult: join(BASE_DIR, "analysis", "analysis-result.json"),
|
|
195
|
+
pending: join(BASE_DIR, "pending"),
|
|
196
|
+
config: join(BASE_DIR, "config.json"),
|
|
197
|
+
counter: join(BASE_DIR, "counter.json"),
|
|
198
|
+
recommendations: join(BASE_DIR, "recommendations.md"),
|
|
199
|
+
recommendationState: join(BASE_DIR, "analysis", "recommendation-state.json"),
|
|
200
|
+
recommendationArchive: join(BASE_DIR, "analysis", "recommendations-archive"),
|
|
201
|
+
notificationFlag: join(BASE_DIR, "analysis", "has-pending-notifications"),
|
|
202
|
+
autoApplyLog: join(BASE_DIR, "analysis", "auto-apply-log.jsonl"),
|
|
203
|
+
outcomeHistory: join(BASE_DIR, "analysis", "outcome-history.jsonl")
|
|
204
|
+
};
|
|
205
|
+
var initialized = false;
|
|
206
|
+
async function ensureInit() {
|
|
207
|
+
if (initialized) return;
|
|
208
|
+
await mkdir(paths.logs.prompts, { recursive: true });
|
|
209
|
+
await mkdir(paths.logs.tools, { recursive: true });
|
|
210
|
+
await mkdir(paths.logs.permissions, { recursive: true });
|
|
211
|
+
await mkdir(paths.logs.sessions, { recursive: true });
|
|
212
|
+
await mkdir(paths.analysis, { recursive: true });
|
|
213
|
+
await mkdir(paths.analysisPreProcessed, { recursive: true });
|
|
214
|
+
await mkdir(paths.pending, { recursive: true });
|
|
215
|
+
await mkdir(paths.recommendationArchive, { recursive: true });
|
|
216
|
+
initialized = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/storage/counter.ts
|
|
220
|
+
async function readCounter() {
|
|
221
|
+
await ensureInit();
|
|
222
|
+
try {
|
|
223
|
+
const raw = await readFile(paths.counter, "utf-8");
|
|
224
|
+
return counterSchema.parse(JSON.parse(raw));
|
|
225
|
+
} catch {
|
|
226
|
+
return {
|
|
227
|
+
total: 0,
|
|
228
|
+
session: {},
|
|
229
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function incrementCounter(sessionId) {
|
|
234
|
+
await ensureInit();
|
|
235
|
+
try {
|
|
236
|
+
await readFile(paths.counter, "utf-8");
|
|
237
|
+
} catch {
|
|
238
|
+
const initial = {
|
|
239
|
+
total: 0,
|
|
240
|
+
session: {},
|
|
241
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
242
|
+
};
|
|
243
|
+
await writeFileAtomic(paths.counter, JSON.stringify(initial, null, 2));
|
|
244
|
+
}
|
|
245
|
+
const release = await lock(paths.counter, {
|
|
246
|
+
retries: { retries: 50, minTimeout: 20, maxTimeout: 1e3, randomize: true },
|
|
247
|
+
stale: 1e4
|
|
248
|
+
// Consider lock stale after 10 seconds
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
const raw = await readFile(paths.counter, "utf-8");
|
|
252
|
+
const data = counterSchema.parse(JSON.parse(raw));
|
|
253
|
+
data.total += 1;
|
|
254
|
+
data.session[sessionId] = (data.session[sessionId] ?? 0) + 1;
|
|
255
|
+
data.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
256
|
+
await writeFileAtomic(paths.counter, JSON.stringify(data, null, 2));
|
|
257
|
+
return data.total;
|
|
258
|
+
} finally {
|
|
259
|
+
await release();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function resetCounter() {
|
|
263
|
+
await ensureInit();
|
|
264
|
+
const data = {
|
|
265
|
+
total: 0,
|
|
266
|
+
session: {},
|
|
267
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
268
|
+
};
|
|
269
|
+
await writeFileAtomic(paths.counter, JSON.stringify(data, null, 2));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/storage/config.ts
|
|
273
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
274
|
+
import writeFileAtomic2 from "write-file-atomic";
|
|
275
|
+
async function loadConfig() {
|
|
276
|
+
try {
|
|
277
|
+
const raw = await readFile2(paths.config, "utf-8");
|
|
278
|
+
return configSchema.parse(JSON.parse(raw));
|
|
279
|
+
} catch {
|
|
280
|
+
const defaults = configSchema.parse({});
|
|
281
|
+
await writeFileAtomic2(paths.config, JSON.stringify(defaults, null, 2));
|
|
282
|
+
return defaults;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/analysis/jsonl-reader.ts
|
|
287
|
+
import { createReadStream } from "fs";
|
|
288
|
+
import { readdir } from "fs/promises";
|
|
289
|
+
import { createInterface } from "readline";
|
|
290
|
+
import { join as join2 } from "path";
|
|
291
|
+
function formatDate(d) {
|
|
292
|
+
return d.toISOString().slice(0, 10);
|
|
293
|
+
}
|
|
294
|
+
async function readLogEntries(logDir, schema, options) {
|
|
295
|
+
let fileNames;
|
|
296
|
+
try {
|
|
297
|
+
fileNames = await readdir(logDir);
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
let jsonlFiles = fileNames.filter((f) => f.endsWith(".jsonl"));
|
|
302
|
+
const sinceStr = options?.since ? formatDate(options.since) : void 0;
|
|
303
|
+
const untilStr = options?.until ? formatDate(options.until) : void 0;
|
|
304
|
+
if (sinceStr || untilStr) {
|
|
305
|
+
jsonlFiles = jsonlFiles.filter((f) => {
|
|
306
|
+
const dateStr = f.replace(".jsonl", "");
|
|
307
|
+
if (sinceStr && dateStr < sinceStr) return false;
|
|
308
|
+
if (untilStr && dateStr > untilStr) return false;
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
jsonlFiles.sort();
|
|
313
|
+
const entries = [];
|
|
314
|
+
for (const file of jsonlFiles) {
|
|
315
|
+
const filePath = join2(logDir, file);
|
|
316
|
+
const rl = createInterface({
|
|
317
|
+
input: createReadStream(filePath, "utf-8"),
|
|
318
|
+
crlfDelay: Infinity
|
|
319
|
+
});
|
|
320
|
+
for await (const line of rl) {
|
|
321
|
+
if (!line.trim()) continue;
|
|
322
|
+
try {
|
|
323
|
+
const parsed = schema.parse(JSON.parse(line));
|
|
324
|
+
entries.push(parsed);
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return entries;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/analysis/schemas.ts
|
|
333
|
+
import { z as z5 } from "zod/v4";
|
|
334
|
+
var summarySchema = z5.object({
|
|
335
|
+
generated_at: z5.iso.datetime(),
|
|
336
|
+
period: z5.object({
|
|
337
|
+
since: z5.string(),
|
|
338
|
+
// YYYY-MM-DD
|
|
339
|
+
until: z5.string(),
|
|
340
|
+
// YYYY-MM-DD
|
|
341
|
+
days: z5.number()
|
|
342
|
+
}),
|
|
343
|
+
stats: z5.object({
|
|
344
|
+
total_prompts: z5.number(),
|
|
345
|
+
total_tool_uses: z5.number(),
|
|
346
|
+
total_permissions: z5.number(),
|
|
347
|
+
unique_sessions: z5.number()
|
|
348
|
+
}),
|
|
349
|
+
top_repeated_prompts: z5.array(
|
|
350
|
+
z5.object({
|
|
351
|
+
prompt: z5.string(),
|
|
352
|
+
count: z5.number(),
|
|
353
|
+
sessions: z5.number()
|
|
354
|
+
})
|
|
355
|
+
).max(20),
|
|
356
|
+
tool_frequency: z5.array(
|
|
357
|
+
z5.object({
|
|
358
|
+
tool_name: z5.string(),
|
|
359
|
+
count: z5.number(),
|
|
360
|
+
avg_duration_ms: z5.number().optional()
|
|
361
|
+
})
|
|
362
|
+
),
|
|
363
|
+
permission_patterns: z5.array(
|
|
364
|
+
z5.object({
|
|
365
|
+
tool_name: z5.string(),
|
|
366
|
+
count: z5.number(),
|
|
367
|
+
sessions: z5.number()
|
|
368
|
+
})
|
|
369
|
+
),
|
|
370
|
+
long_prompts: z5.array(
|
|
371
|
+
z5.object({
|
|
372
|
+
prompt_preview: z5.string(),
|
|
373
|
+
length: z5.number(),
|
|
374
|
+
count: z5.number()
|
|
375
|
+
})
|
|
376
|
+
).max(10)
|
|
377
|
+
});
|
|
378
|
+
var environmentSnapshotSchema = z5.object({
|
|
379
|
+
generated_at: z5.iso.datetime(),
|
|
380
|
+
claude_code: z5.object({
|
|
381
|
+
version: z5.string(),
|
|
382
|
+
version_known: z5.boolean(),
|
|
383
|
+
compatible: z5.boolean()
|
|
384
|
+
}),
|
|
385
|
+
settings: z5.object({
|
|
386
|
+
user: z5.unknown().nullable(),
|
|
387
|
+
project: z5.unknown().nullable(),
|
|
388
|
+
local: z5.unknown().nullable()
|
|
389
|
+
}),
|
|
390
|
+
installed_tools: z5.object({
|
|
391
|
+
plugins: z5.array(
|
|
392
|
+
z5.object({
|
|
393
|
+
name: z5.string(),
|
|
394
|
+
marketplace: z5.string(),
|
|
395
|
+
enabled: z5.boolean(),
|
|
396
|
+
scope: z5.string(),
|
|
397
|
+
capabilities: z5.array(z5.string())
|
|
398
|
+
})
|
|
399
|
+
),
|
|
400
|
+
skills: z5.array(
|
|
401
|
+
z5.object({
|
|
402
|
+
name: z5.string(),
|
|
403
|
+
scope: z5.enum(["user", "project"])
|
|
404
|
+
})
|
|
405
|
+
),
|
|
406
|
+
rules: z5.array(
|
|
407
|
+
z5.object({
|
|
408
|
+
name: z5.string(),
|
|
409
|
+
scope: z5.enum(["user", "project"])
|
|
410
|
+
})
|
|
411
|
+
),
|
|
412
|
+
hooks: z5.array(
|
|
413
|
+
z5.object({
|
|
414
|
+
event: z5.string(),
|
|
415
|
+
scope: z5.enum(["user", "project", "local"]),
|
|
416
|
+
type: z5.string()
|
|
417
|
+
})
|
|
418
|
+
),
|
|
419
|
+
claude_md: z5.array(
|
|
420
|
+
z5.object({
|
|
421
|
+
path: z5.string(),
|
|
422
|
+
exists: z5.boolean()
|
|
423
|
+
})
|
|
424
|
+
)
|
|
425
|
+
}),
|
|
426
|
+
detected_ecosystems: z5.array(z5.string())
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// src/analysis/pre-processor.ts
|
|
430
|
+
import writeFileAtomic3 from "write-file-atomic";
|
|
431
|
+
var PROMPT_TRUNCATE_LEN = 100;
|
|
432
|
+
var LONG_PROMPT_THRESHOLD = 200;
|
|
433
|
+
var DEFAULT_TOP_N = 20;
|
|
434
|
+
var DEFAULT_DAYS = 30;
|
|
435
|
+
var MAX_LONG_PROMPTS = 10;
|
|
436
|
+
function normalizePrompt(prompt) {
|
|
437
|
+
return prompt.trim().toLowerCase().replace(/\s+/g, " ");
|
|
438
|
+
}
|
|
439
|
+
function formatDate2(d) {
|
|
440
|
+
return d.toISOString().slice(0, 10);
|
|
441
|
+
}
|
|
442
|
+
function countWithSessions(items) {
|
|
443
|
+
const map = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const { key, session } of items) {
|
|
445
|
+
const existing = map.get(key);
|
|
446
|
+
if (existing) {
|
|
447
|
+
existing.count += 1;
|
|
448
|
+
existing.sessions.add(session);
|
|
449
|
+
} else {
|
|
450
|
+
map.set(key, { count: 1, sessions: /* @__PURE__ */ new Set([session]) });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return map;
|
|
454
|
+
}
|
|
455
|
+
function computeToolFrequency(tools) {
|
|
456
|
+
const map = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const entry of tools) {
|
|
458
|
+
const existing = map.get(entry.tool_name);
|
|
459
|
+
if (existing) {
|
|
460
|
+
existing.count += 1;
|
|
461
|
+
if (entry.event === "post" && entry.duration_ms != null) {
|
|
462
|
+
existing.durations.push(entry.duration_ms);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
const durations = [];
|
|
466
|
+
if (entry.event === "post" && entry.duration_ms != null) {
|
|
467
|
+
durations.push(entry.duration_ms);
|
|
468
|
+
}
|
|
469
|
+
map.set(entry.tool_name, { count: 1, durations });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return Array.from(map.entries()).map(([tool_name, { count, durations }]) => ({
|
|
473
|
+
tool_name,
|
|
474
|
+
count,
|
|
475
|
+
avg_duration_ms: durations.length > 0 ? Math.round(
|
|
476
|
+
durations.reduce((sum, d) => sum + d, 0) / durations.length
|
|
477
|
+
) : void 0
|
|
478
|
+
})).sort((a, b) => b.count - a.count);
|
|
479
|
+
}
|
|
480
|
+
function detectLongPrompts(prompts) {
|
|
481
|
+
const map = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const entry of prompts) {
|
|
483
|
+
const words = entry.prompt.trim().split(/\s+/);
|
|
484
|
+
if (words.length <= LONG_PROMPT_THRESHOLD) continue;
|
|
485
|
+
const key = normalizePrompt(entry.prompt);
|
|
486
|
+
const existing = map.get(key);
|
|
487
|
+
if (existing) {
|
|
488
|
+
existing.count += 1;
|
|
489
|
+
} else {
|
|
490
|
+
map.set(key, { length: words.length, count: 1 });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return Array.from(map.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, MAX_LONG_PROMPTS).map(([normalized, { length, count }]) => ({
|
|
494
|
+
prompt_preview: normalized.slice(0, PROMPT_TRUNCATE_LEN),
|
|
495
|
+
length,
|
|
496
|
+
count
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
async function preProcess(options) {
|
|
500
|
+
const until = options?.until ?? /* @__PURE__ */ new Date();
|
|
501
|
+
const since = options?.since ?? new Date(until.getTime() - DEFAULT_DAYS * 864e5);
|
|
502
|
+
const topN = options?.topN ?? DEFAULT_TOP_N;
|
|
503
|
+
const [prompts, tools, permissions] = await Promise.all([
|
|
504
|
+
readLogEntries(paths.logs.prompts, promptEntrySchema, { since, until }),
|
|
505
|
+
readLogEntries(paths.logs.tools, toolEntrySchema, { since, until }),
|
|
506
|
+
readLogEntries(paths.logs.permissions, permissionEntrySchema, {
|
|
507
|
+
since,
|
|
508
|
+
until
|
|
509
|
+
})
|
|
510
|
+
]);
|
|
511
|
+
const sessionSet = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const p of prompts) sessionSet.add(p.session_id);
|
|
513
|
+
for (const t of tools) sessionSet.add(t.session_id);
|
|
514
|
+
for (const perm of permissions) sessionSet.add(perm.session_id);
|
|
515
|
+
const promptCounts = countWithSessions(
|
|
516
|
+
prompts.map((p) => ({
|
|
517
|
+
key: normalizePrompt(p.prompt),
|
|
518
|
+
session: p.session_id
|
|
519
|
+
}))
|
|
520
|
+
);
|
|
521
|
+
const topRepeatedPrompts = Array.from(promptCounts.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, topN).map(([key, { count, sessions }]) => ({
|
|
522
|
+
prompt: key.slice(0, PROMPT_TRUNCATE_LEN),
|
|
523
|
+
count,
|
|
524
|
+
sessions: sessions.size
|
|
525
|
+
}));
|
|
526
|
+
const toolFrequency = computeToolFrequency(tools);
|
|
527
|
+
const permissionCounts = countWithSessions(
|
|
528
|
+
permissions.map((p) => ({
|
|
529
|
+
key: p.tool_name,
|
|
530
|
+
session: p.session_id
|
|
531
|
+
}))
|
|
532
|
+
);
|
|
533
|
+
const permissionPatterns = Array.from(permissionCounts.entries()).sort((a, b) => b[1].count - a[1].count).map(([tool_name, { count, sessions }]) => ({
|
|
534
|
+
tool_name,
|
|
535
|
+
count,
|
|
536
|
+
sessions: sessions.size
|
|
537
|
+
}));
|
|
538
|
+
const longPrompts = detectLongPrompts(prompts);
|
|
539
|
+
const summary = summarySchema.parse({
|
|
540
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
541
|
+
period: {
|
|
542
|
+
since: formatDate2(since),
|
|
543
|
+
until: formatDate2(until),
|
|
544
|
+
days: DEFAULT_DAYS
|
|
545
|
+
},
|
|
546
|
+
stats: {
|
|
547
|
+
total_prompts: prompts.length,
|
|
548
|
+
total_tool_uses: tools.length,
|
|
549
|
+
total_permissions: permissions.length,
|
|
550
|
+
unique_sessions: sessionSet.size
|
|
551
|
+
},
|
|
552
|
+
top_repeated_prompts: topRepeatedPrompts,
|
|
553
|
+
tool_frequency: toolFrequency,
|
|
554
|
+
permission_patterns: permissionPatterns,
|
|
555
|
+
long_prompts: longPrompts
|
|
556
|
+
});
|
|
557
|
+
await ensureInit();
|
|
558
|
+
await writeFileAtomic3(paths.summary, JSON.stringify(summary));
|
|
559
|
+
return summary;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/analysis/environment-scanner.ts
|
|
563
|
+
import { readdir as readdir2, readFile as readFile3, access } from "fs/promises";
|
|
564
|
+
import { execFileSync } from "child_process";
|
|
565
|
+
import { join as join3 } from "path";
|
|
566
|
+
import { constants } from "fs";
|
|
567
|
+
import writeFileAtomic4 from "write-file-atomic";
|
|
568
|
+
var KNOWN_COMPATIBLE_MIN = "2.1.0";
|
|
569
|
+
var KNOWN_COMPATIBLE_MAX = "2.1.99";
|
|
570
|
+
async function scanEnvironment(cwd, home) {
|
|
571
|
+
const homeDir = home ?? process.env.HOME ?? "";
|
|
572
|
+
const [userSettings, projectSettings, localSettings] = await Promise.all([
|
|
573
|
+
readSettingsSafe(join3(homeDir, ".claude", "settings.json")),
|
|
574
|
+
readSettingsSafe(join3(cwd, ".claude", "settings.json")),
|
|
575
|
+
readSettingsSafe(join3(cwd, ".claude", "settings.local.json"))
|
|
576
|
+
]);
|
|
577
|
+
const enabledPluginNames = extractEnabledPlugins(userSettings);
|
|
578
|
+
const [claudeVersion, plugins, skills, rules, hooks, claudeMds, ecosystems] = await Promise.all([
|
|
579
|
+
Promise.resolve(detectClaudeCodeVersion()),
|
|
580
|
+
discoverPlugins(homeDir, enabledPluginNames),
|
|
581
|
+
discoverSkills(homeDir, cwd),
|
|
582
|
+
discoverRules(cwd),
|
|
583
|
+
Promise.resolve(
|
|
584
|
+
discoverHooks(userSettings, projectSettings, localSettings)
|
|
585
|
+
),
|
|
586
|
+
discoverClaudeMd(homeDir, cwd),
|
|
587
|
+
detectEcosystems(cwd, homeDir)
|
|
588
|
+
]);
|
|
589
|
+
const snapshot = {
|
|
590
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
591
|
+
claude_code: claudeVersion,
|
|
592
|
+
settings: {
|
|
593
|
+
user: userSettings,
|
|
594
|
+
project: projectSettings,
|
|
595
|
+
local: localSettings
|
|
596
|
+
},
|
|
597
|
+
installed_tools: {
|
|
598
|
+
plugins,
|
|
599
|
+
skills,
|
|
600
|
+
rules,
|
|
601
|
+
hooks,
|
|
602
|
+
claude_md: claudeMds
|
|
603
|
+
},
|
|
604
|
+
detected_ecosystems: ecosystems
|
|
605
|
+
};
|
|
606
|
+
const validated = environmentSnapshotSchema.parse(snapshot);
|
|
607
|
+
await ensureInit();
|
|
608
|
+
await writeFileAtomic4(
|
|
609
|
+
paths.environmentSnapshot,
|
|
610
|
+
JSON.stringify(validated)
|
|
611
|
+
);
|
|
612
|
+
return validated;
|
|
613
|
+
}
|
|
614
|
+
function detectClaudeCodeVersion() {
|
|
615
|
+
try {
|
|
616
|
+
const output = execFileSync("claude", ["--version"], {
|
|
617
|
+
timeout: 3e3,
|
|
618
|
+
encoding: "utf-8",
|
|
619
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
620
|
+
}).trim();
|
|
621
|
+
const match = output.match(/^(\d+\.\d+\.\d+)/);
|
|
622
|
+
if (!match) {
|
|
623
|
+
return { version: "unknown", version_known: false, compatible: false };
|
|
624
|
+
}
|
|
625
|
+
const version = match[1];
|
|
626
|
+
const compatible = compareSemver(version, KNOWN_COMPATIBLE_MIN) >= 0 && compareSemver(version, KNOWN_COMPATIBLE_MAX) <= 0;
|
|
627
|
+
return { version, version_known: true, compatible };
|
|
628
|
+
} catch {
|
|
629
|
+
return { version: "unknown", version_known: false, compatible: false };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function readSettingsSafe(filePath) {
|
|
633
|
+
try {
|
|
634
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
635
|
+
return JSON.parse(raw);
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function extractEnabledPlugins(settings) {
|
|
641
|
+
if (!settings || typeof settings !== "object") return [];
|
|
642
|
+
const obj = settings;
|
|
643
|
+
if (!Array.isArray(obj.enabledPlugins)) return [];
|
|
644
|
+
return obj.enabledPlugins.map((p) => {
|
|
645
|
+
if (typeof p === "string") return p;
|
|
646
|
+
if (p && typeof p === "object" && "name" in p) {
|
|
647
|
+
return String(p.name);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}).filter((n) => n !== null);
|
|
651
|
+
}
|
|
652
|
+
async function discoverPlugins(home, enabledPluginNames) {
|
|
653
|
+
try {
|
|
654
|
+
const pluginsFile = join3(home, ".claude", "plugins", "installed_plugins.json");
|
|
655
|
+
const raw = await readFile3(pluginsFile, "utf-8");
|
|
656
|
+
const installed = JSON.parse(raw);
|
|
657
|
+
if (!Array.isArray(installed)) return [];
|
|
658
|
+
const plugins = [];
|
|
659
|
+
for (const entry of installed) {
|
|
660
|
+
if (!entry || typeof entry !== "object") continue;
|
|
661
|
+
const plugin = entry;
|
|
662
|
+
const name = String(plugin.name ?? "");
|
|
663
|
+
const marketplace = String(plugin.marketplace ?? "unknown");
|
|
664
|
+
const scope = String(plugin.scope ?? "user");
|
|
665
|
+
const version = String(plugin.version ?? "latest");
|
|
666
|
+
const enabled = enabledPluginNames.includes(name);
|
|
667
|
+
const capabilities = await scanPluginCapabilities(
|
|
668
|
+
home,
|
|
669
|
+
marketplace,
|
|
670
|
+
name,
|
|
671
|
+
version
|
|
672
|
+
);
|
|
673
|
+
plugins.push({ name, marketplace, enabled, scope, capabilities });
|
|
674
|
+
}
|
|
675
|
+
return plugins;
|
|
676
|
+
} catch {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function scanPluginCapabilities(home, marketplace, pluginName, version) {
|
|
681
|
+
const knownCapabilities = ["commands", "skills", "hooks", "agents"];
|
|
682
|
+
const capabilities = [];
|
|
683
|
+
try {
|
|
684
|
+
const cacheDir = join3(
|
|
685
|
+
home,
|
|
686
|
+
".claude",
|
|
687
|
+
"plugins",
|
|
688
|
+
"cache",
|
|
689
|
+
marketplace,
|
|
690
|
+
pluginName,
|
|
691
|
+
version
|
|
692
|
+
);
|
|
693
|
+
const entries = await readdir2(cacheDir, { withFileTypes: true });
|
|
694
|
+
for (const entry of entries) {
|
|
695
|
+
if (entry.isDirectory() && knownCapabilities.includes(entry.name)) {
|
|
696
|
+
capabilities.push(entry.name);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
return capabilities;
|
|
702
|
+
}
|
|
703
|
+
async function discoverSkills(home, cwd) {
|
|
704
|
+
const skills = [];
|
|
705
|
+
try {
|
|
706
|
+
const userSkillsDir = join3(home, ".claude", "skills");
|
|
707
|
+
const entries = await readdir2(userSkillsDir, { withFileTypes: true });
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
if (entry.isDirectory()) {
|
|
710
|
+
skills.push({ name: entry.name, scope: "user" });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const projectSkillsDir = join3(cwd, ".claude", "skills");
|
|
717
|
+
const entries = await readdir2(projectSkillsDir, { withFileTypes: true });
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
if (entry.isDirectory()) {
|
|
720
|
+
skills.push({ name: entry.name, scope: "project" });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
return skills;
|
|
726
|
+
}
|
|
727
|
+
async function discoverRules(cwd) {
|
|
728
|
+
const rules = [];
|
|
729
|
+
try {
|
|
730
|
+
const rulesDir = join3(cwd, ".claude", "rules");
|
|
731
|
+
const entries = await readdir2(rulesDir, { withFileTypes: true });
|
|
732
|
+
for (const entry of entries) {
|
|
733
|
+
if (entry.isDirectory()) {
|
|
734
|
+
rules.push({ name: entry.name, scope: "project" });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
return rules;
|
|
740
|
+
}
|
|
741
|
+
function discoverHooks(userSettings, projectSettings, localSettings) {
|
|
742
|
+
const hooks = [];
|
|
743
|
+
extractHooksFromSettings(userSettings, "user", hooks);
|
|
744
|
+
extractHooksFromSettings(projectSettings, "project", hooks);
|
|
745
|
+
extractHooksFromSettings(localSettings, "local", hooks);
|
|
746
|
+
return hooks;
|
|
747
|
+
}
|
|
748
|
+
function extractHooksFromSettings(settings, scope, hooks) {
|
|
749
|
+
if (!settings || typeof settings !== "object") return;
|
|
750
|
+
const obj = settings;
|
|
751
|
+
if (!obj.hooks || typeof obj.hooks !== "object") return;
|
|
752
|
+
const hooksConfig = obj.hooks;
|
|
753
|
+
for (const [event, defs] of Object.entries(hooksConfig)) {
|
|
754
|
+
if (!Array.isArray(defs)) continue;
|
|
755
|
+
for (const def of defs) {
|
|
756
|
+
if (!def || typeof def !== "object") continue;
|
|
757
|
+
const hookDef = def;
|
|
758
|
+
const type = String(hookDef.type ?? "command");
|
|
759
|
+
hooks.push({ event, scope, type });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function discoverClaudeMd(home, cwd) {
|
|
764
|
+
const locations = [
|
|
765
|
+
join3(cwd, "CLAUDE.md"),
|
|
766
|
+
join3(cwd, ".claude", "CLAUDE.md"),
|
|
767
|
+
join3(home, ".claude", "CLAUDE.md")
|
|
768
|
+
];
|
|
769
|
+
const results = [];
|
|
770
|
+
for (const path of locations) {
|
|
771
|
+
let exists = false;
|
|
772
|
+
try {
|
|
773
|
+
await access(path, constants.F_OK);
|
|
774
|
+
exists = true;
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
results.push({ path, exists });
|
|
778
|
+
}
|
|
779
|
+
return results;
|
|
780
|
+
}
|
|
781
|
+
async function detectEcosystems(cwd, home) {
|
|
782
|
+
const ecosystems = [];
|
|
783
|
+
try {
|
|
784
|
+
await access(join3(cwd, ".planning"), constants.F_OK);
|
|
785
|
+
ecosystems.push("gsd");
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
const skillsDir = join3(home, ".claude", "skills");
|
|
790
|
+
const entries = await readdir2(skillsDir);
|
|
791
|
+
if (entries.some((e) => e.toLowerCase().includes("cog"))) {
|
|
792
|
+
ecosystems.push("cog");
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
return ecosystems;
|
|
797
|
+
}
|
|
798
|
+
function compareSemver(a, b) {
|
|
799
|
+
const partsA = a.split(".").map(Number);
|
|
800
|
+
const partsB = b.split(".").map(Number);
|
|
801
|
+
for (let i = 0; i < 3; i++) {
|
|
802
|
+
const numA = partsA[i] ?? 0;
|
|
803
|
+
const numB = partsB[i] ?? 0;
|
|
804
|
+
if (numA < numB) return -1;
|
|
805
|
+
if (numA > numB) return 1;
|
|
806
|
+
}
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/analysis/classifiers/repeated-prompts.ts
|
|
811
|
+
function truncate2(str, maxLen) {
|
|
812
|
+
if (str.length <= maxLen) return str;
|
|
813
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
814
|
+
}
|
|
815
|
+
function classifyRepeatedPrompts(summary, _snapshot, config) {
|
|
816
|
+
const recommendations = [];
|
|
817
|
+
const threshold = config.thresholds.repeated_prompt_min_count;
|
|
818
|
+
for (let i = 0; i < summary.top_repeated_prompts.length; i++) {
|
|
819
|
+
const entry = summary.top_repeated_prompts[i];
|
|
820
|
+
if (entry.count < threshold) continue;
|
|
821
|
+
const wordCount = entry.prompt.split(/\s+/).length;
|
|
822
|
+
if (wordCount > 50) continue;
|
|
823
|
+
const confidence = entry.count >= config.thresholds.repeated_prompt_high_count && entry.sessions >= config.thresholds.repeated_prompt_high_sessions ? "HIGH" : "MEDIUM";
|
|
824
|
+
const truncatedPrompt = truncate2(entry.prompt, 60);
|
|
825
|
+
recommendations.push({
|
|
826
|
+
id: `rec-repeated-${i}`,
|
|
827
|
+
target: "HOOK",
|
|
828
|
+
confidence,
|
|
829
|
+
pattern_type: "repeated_prompt",
|
|
830
|
+
title: `Repeated prompt: "${truncatedPrompt}"`,
|
|
831
|
+
description: `This prompt has been used ${entry.count} times across ${entry.sessions} sessions. Consider creating a hook or alias to automate this.`,
|
|
832
|
+
evidence: {
|
|
833
|
+
count: entry.count,
|
|
834
|
+
sessions: entry.sessions,
|
|
835
|
+
examples: [entry.prompt]
|
|
836
|
+
},
|
|
837
|
+
suggested_action: `Create a UserPromptSubmit hook that detects "${truncatedPrompt}" and auto-executes the intended action.`
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
return recommendations;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/analysis/classifiers/long-prompts.ts
|
|
844
|
+
function classifyLongPrompts(summary, _snapshot, config) {
|
|
845
|
+
const recommendations = [];
|
|
846
|
+
for (let i = 0; i < summary.long_prompts.length; i++) {
|
|
847
|
+
const entry = summary.long_prompts[i];
|
|
848
|
+
if (entry.length < config.thresholds.long_prompt_min_words) continue;
|
|
849
|
+
if (entry.count < config.thresholds.long_prompt_min_count) continue;
|
|
850
|
+
const confidence = entry.count >= config.thresholds.long_prompt_high_count && entry.length >= config.thresholds.long_prompt_high_words ? "HIGH" : "MEDIUM";
|
|
851
|
+
recommendations.push({
|
|
852
|
+
id: `rec-long-${i}`,
|
|
853
|
+
target: "SKILL",
|
|
854
|
+
confidence,
|
|
855
|
+
pattern_type: "long_prompt",
|
|
856
|
+
title: `Long repeated prompt (${entry.length} words, ${entry.count}x)`,
|
|
857
|
+
description: `A ${entry.length}-word prompt has been used ${entry.count} times. Consider converting it to a reusable skill.`,
|
|
858
|
+
evidence: {
|
|
859
|
+
count: entry.count,
|
|
860
|
+
examples: [entry.prompt_preview]
|
|
861
|
+
},
|
|
862
|
+
suggested_action: "Create a skill in .claude/skills/ that encapsulates this prompt as a reusable workflow."
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return recommendations;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/analysis/classifiers/permission-patterns.ts
|
|
869
|
+
function classifyPermissionPatterns(summary, _snapshot, config) {
|
|
870
|
+
const recommendations = [];
|
|
871
|
+
for (let i = 0; i < summary.permission_patterns.length; i++) {
|
|
872
|
+
const entry = summary.permission_patterns[i];
|
|
873
|
+
if (entry.count < config.thresholds.permission_approval_min_count) continue;
|
|
874
|
+
if (entry.sessions < config.thresholds.permission_approval_min_sessions) continue;
|
|
875
|
+
const confidence = entry.count >= config.thresholds.permission_approval_high_count && entry.sessions >= config.thresholds.permission_approval_high_sessions ? "HIGH" : "MEDIUM";
|
|
876
|
+
recommendations.push({
|
|
877
|
+
id: `rec-permission-always-approved-${i}`,
|
|
878
|
+
target: "SETTINGS",
|
|
879
|
+
confidence,
|
|
880
|
+
pattern_type: "permission-always-approved",
|
|
881
|
+
title: `Frequently approved tool: ${entry.tool_name}`,
|
|
882
|
+
description: `You have approved "${entry.tool_name}" ${entry.count} times across ${entry.sessions} sessions. Consider adding it to allowedTools in settings.json.`,
|
|
883
|
+
evidence: {
|
|
884
|
+
count: entry.count,
|
|
885
|
+
sessions: entry.sessions,
|
|
886
|
+
examples: [`${entry.tool_name} approved ${entry.count} times`]
|
|
887
|
+
},
|
|
888
|
+
suggested_action: `Add "${entry.tool_name}" to the "allow" array in ~/.claude/settings.json permissions.`
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return recommendations;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/analysis/classifiers/code-corrections.ts
|
|
895
|
+
var CODE_MODIFICATION_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
|
|
896
|
+
var HIGH_USAGE_THRESHOLD = 20;
|
|
897
|
+
function classifyCodeCorrections(summary, _snapshot, _config) {
|
|
898
|
+
const recommendations = [];
|
|
899
|
+
let index = 0;
|
|
900
|
+
for (const entry of summary.tool_frequency) {
|
|
901
|
+
if (!CODE_MODIFICATION_TOOLS.has(entry.tool_name)) continue;
|
|
902
|
+
if (entry.count < HIGH_USAGE_THRESHOLD) continue;
|
|
903
|
+
recommendations.push({
|
|
904
|
+
id: `rec-correction-${index}`,
|
|
905
|
+
target: "RULE",
|
|
906
|
+
confidence: "LOW",
|
|
907
|
+
pattern_type: "code_correction",
|
|
908
|
+
title: `Frequent code modifications with ${entry.tool_name} (${entry.count} uses)`,
|
|
909
|
+
description: `The ${entry.tool_name} tool has been used ${entry.count} times. Review for recurring patterns that could become a coding rule or convention.`,
|
|
910
|
+
evidence: {
|
|
911
|
+
count: entry.count,
|
|
912
|
+
examples: [entry.tool_name]
|
|
913
|
+
},
|
|
914
|
+
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/.`
|
|
915
|
+
});
|
|
916
|
+
index++;
|
|
917
|
+
}
|
|
918
|
+
return recommendations;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/analysis/classifiers/personal-info.ts
|
|
922
|
+
var PERSONAL_KEYWORDS = [
|
|
923
|
+
"my name is",
|
|
924
|
+
"i live in",
|
|
925
|
+
"i work at",
|
|
926
|
+
"i prefer",
|
|
927
|
+
"my email",
|
|
928
|
+
"my project",
|
|
929
|
+
"always use",
|
|
930
|
+
"never use"
|
|
931
|
+
];
|
|
932
|
+
var MIN_COUNT = 2;
|
|
933
|
+
function classifyPersonalInfo(summary, _snapshot, _config) {
|
|
934
|
+
const recommendations = [];
|
|
935
|
+
const matchedKeywords = /* @__PURE__ */ new Set();
|
|
936
|
+
let index = 0;
|
|
937
|
+
for (const entry of summary.top_repeated_prompts) {
|
|
938
|
+
if (entry.count < MIN_COUNT) continue;
|
|
939
|
+
const lowerPrompt = entry.prompt.toLowerCase();
|
|
940
|
+
for (const keyword of PERSONAL_KEYWORDS) {
|
|
941
|
+
if (matchedKeywords.has(keyword)) continue;
|
|
942
|
+
if (!lowerPrompt.includes(keyword)) continue;
|
|
943
|
+
matchedKeywords.add(keyword);
|
|
944
|
+
recommendations.push({
|
|
945
|
+
id: `rec-personal-${index}`,
|
|
946
|
+
target: "MEMORY",
|
|
947
|
+
confidence: "LOW",
|
|
948
|
+
pattern_type: "personal_info",
|
|
949
|
+
title: `Personal preference detected: "${keyword}..."`,
|
|
950
|
+
description: `A prompt mentioning personal information ("${keyword}") has appeared ${entry.count} times. Consider storing this in memory for automatic context.`,
|
|
951
|
+
evidence: {
|
|
952
|
+
count: entry.count,
|
|
953
|
+
sessions: entry.sessions,
|
|
954
|
+
examples: [entry.prompt]
|
|
955
|
+
},
|
|
956
|
+
suggested_action: "Add this information to memory (e.g., CLAUDE.md or a memory file) so Claude Code can use it without being reminded."
|
|
957
|
+
});
|
|
958
|
+
index++;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return recommendations;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/analysis/classifiers/config-drift.ts
|
|
965
|
+
var MAX_HOOKS_BEFORE_REVIEW = 10;
|
|
966
|
+
function classifyConfigDrift(_summary, snapshot, _config) {
|
|
967
|
+
const recommendations = [];
|
|
968
|
+
let index = 0;
|
|
969
|
+
const hookEvents = new Set(snapshot.installed_tools.hooks.map((h) => h.event));
|
|
970
|
+
const ruleNames = new Set(snapshot.installed_tools.rules.map((r) => r.name));
|
|
971
|
+
for (const overlap of hookEvents) {
|
|
972
|
+
if (!ruleNames.has(overlap)) continue;
|
|
973
|
+
recommendations.push({
|
|
974
|
+
id: `rec-drift-${index}`,
|
|
975
|
+
target: "RULE",
|
|
976
|
+
confidence: "LOW",
|
|
977
|
+
pattern_type: "config_drift",
|
|
978
|
+
title: `Hook-rule overlap detected: "${overlap}"`,
|
|
979
|
+
description: `Both a hook (event: "${overlap}") and a rule (name: "${overlap}") exist. This may indicate duplicated behavior that should be consolidated into one mechanism.`,
|
|
980
|
+
evidence: {
|
|
981
|
+
count: 2,
|
|
982
|
+
examples: [`Hook event: ${overlap}`, `Rule name: ${overlap}`]
|
|
983
|
+
},
|
|
984
|
+
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).`
|
|
985
|
+
});
|
|
986
|
+
index++;
|
|
987
|
+
}
|
|
988
|
+
const existingClaudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists);
|
|
989
|
+
if (existingClaudeMd.length > 1) {
|
|
990
|
+
recommendations.push({
|
|
991
|
+
id: `rec-drift-${index}`,
|
|
992
|
+
target: "CLAUDE_MD",
|
|
993
|
+
confidence: "LOW",
|
|
994
|
+
pattern_type: "config_drift",
|
|
995
|
+
title: `Multiple CLAUDE.md files detected (${existingClaudeMd.length})`,
|
|
996
|
+
description: `Found ${existingClaudeMd.length} existing CLAUDE.md files. Multiple CLAUDE.md files may contain contradictory instructions. Review for consistency.`,
|
|
997
|
+
evidence: {
|
|
998
|
+
count: existingClaudeMd.length,
|
|
999
|
+
examples: existingClaudeMd.slice(0, 3).map((c) => c.path)
|
|
1000
|
+
},
|
|
1001
|
+
suggested_action: "Review all CLAUDE.md files for contradictions or redundancies. Consider consolidating shared instructions into the most appropriate scope."
|
|
1002
|
+
});
|
|
1003
|
+
index++;
|
|
1004
|
+
}
|
|
1005
|
+
if (snapshot.installed_tools.hooks.length > MAX_HOOKS_BEFORE_REVIEW) {
|
|
1006
|
+
recommendations.push({
|
|
1007
|
+
id: `rec-drift-${index}`,
|
|
1008
|
+
target: "HOOK",
|
|
1009
|
+
confidence: "LOW",
|
|
1010
|
+
pattern_type: "config_drift",
|
|
1011
|
+
title: `Excessive hook count (${snapshot.installed_tools.hooks.length} hooks)`,
|
|
1012
|
+
description: `Found ${snapshot.installed_tools.hooks.length} hooks across all scopes. This many hooks may indicate redundancy or performance concerns. Review for consolidation.`,
|
|
1013
|
+
evidence: {
|
|
1014
|
+
count: snapshot.installed_tools.hooks.length,
|
|
1015
|
+
examples: snapshot.installed_tools.hooks.slice(0, 3).map((h) => `${h.event} (${h.scope})`)
|
|
1016
|
+
},
|
|
1017
|
+
suggested_action: "Review all hooks for overlapping functionality. Consider combining hooks that trigger on the same event or serve similar purposes."
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return recommendations;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/analysis/classifiers/ecosystem-adapter.ts
|
|
1024
|
+
var MULTI_STEP_MIN_COUNT = 3;
|
|
1025
|
+
function classifyEcosystemAdaptations(summary, snapshot, _config) {
|
|
1026
|
+
const recommendations = [];
|
|
1027
|
+
let index = 0;
|
|
1028
|
+
if (snapshot.claude_code.version_known && !snapshot.claude_code.compatible && snapshot.claude_code.version !== "unknown") {
|
|
1029
|
+
recommendations.push({
|
|
1030
|
+
id: `rec-ecosystem-${index}`,
|
|
1031
|
+
target: "CLAUDE_MD",
|
|
1032
|
+
confidence: "MEDIUM",
|
|
1033
|
+
pattern_type: "version_update",
|
|
1034
|
+
title: `Claude Code version ${snapshot.claude_code.version} detected (outside tested range)`,
|
|
1035
|
+
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.`,
|
|
1036
|
+
evidence: {
|
|
1037
|
+
count: 1,
|
|
1038
|
+
examples: [`Version: ${snapshot.claude_code.version}`]
|
|
1039
|
+
},
|
|
1040
|
+
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.`
|
|
1041
|
+
});
|
|
1042
|
+
index++;
|
|
1043
|
+
}
|
|
1044
|
+
if (snapshot.detected_ecosystems.includes("gsd")) {
|
|
1045
|
+
const multiStepPrompts = summary.top_repeated_prompts.filter(
|
|
1046
|
+
(p) => p.count >= MULTI_STEP_MIN_COUNT
|
|
1047
|
+
);
|
|
1048
|
+
if (multiStepPrompts.length > 0) {
|
|
1049
|
+
const topPrompt = multiStepPrompts[0];
|
|
1050
|
+
recommendations.push({
|
|
1051
|
+
id: `rec-ecosystem-${index}`,
|
|
1052
|
+
target: "SKILL",
|
|
1053
|
+
confidence: "LOW",
|
|
1054
|
+
pattern_type: "ecosystem_gsd",
|
|
1055
|
+
title: "GSD workflow detected -- consider /gsd slash commands",
|
|
1056
|
+
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.",
|
|
1057
|
+
evidence: {
|
|
1058
|
+
count: multiStepPrompts.length,
|
|
1059
|
+
examples: [topPrompt.prompt]
|
|
1060
|
+
},
|
|
1061
|
+
suggested_action: "Review repeated prompts and consider if they map to GSD phases (/gsd:plan-phase, /gsd:execute-phase) or custom slash commands.",
|
|
1062
|
+
ecosystem_context: "GSD detected: Use /gsd slash commands and .planning patterns for multi-step workflows instead of standalone skills"
|
|
1063
|
+
});
|
|
1064
|
+
index++;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (snapshot.detected_ecosystems.includes("cog")) {
|
|
1068
|
+
recommendations.push({
|
|
1069
|
+
id: `rec-ecosystem-${index}`,
|
|
1070
|
+
target: "MEMORY",
|
|
1071
|
+
confidence: "LOW",
|
|
1072
|
+
pattern_type: "ecosystem_cog",
|
|
1073
|
+
title: "Cog memory system detected -- route memory to Cog tiers",
|
|
1074
|
+
description: "Cog is installed. Personal information and contextual preferences should be routed to Cog's tiered memory system rather than raw CLAUDE.md entries.",
|
|
1075
|
+
evidence: {
|
|
1076
|
+
count: 1,
|
|
1077
|
+
examples: ["Cog detected in ~/.claude/skills/"]
|
|
1078
|
+
},
|
|
1079
|
+
suggested_action: "Use /reflect and /evolve Cog commands for memory management instead of manually editing CLAUDE.md.",
|
|
1080
|
+
ecosystem_context: "Cog detected: Route memory entries to Cog tiers (/reflect, /evolve) instead of raw CLAUDE.md"
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return recommendations;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/analysis/experience-level.ts
|
|
1087
|
+
function computeExperienceLevel(snapshot) {
|
|
1088
|
+
const hooks = snapshot.installed_tools.hooks.length;
|
|
1089
|
+
const rules = snapshot.installed_tools.rules.length;
|
|
1090
|
+
const skills = snapshot.installed_tools.skills.length;
|
|
1091
|
+
const plugins = snapshot.installed_tools.plugins.length;
|
|
1092
|
+
const claudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists).length;
|
|
1093
|
+
const ecosystems = snapshot.detected_ecosystems.length;
|
|
1094
|
+
const score = Math.min(
|
|
1095
|
+
100,
|
|
1096
|
+
hooks * 8 + rules * 6 + skills * 5 + plugins * 10 + claudeMd * 3 + ecosystems * 7
|
|
1097
|
+
);
|
|
1098
|
+
const tier = score === 0 ? "newcomer" : score < 30 ? "intermediate" : "power_user";
|
|
1099
|
+
return {
|
|
1100
|
+
tier,
|
|
1101
|
+
score,
|
|
1102
|
+
breakdown: { hooks, rules, skills, plugins, claude_md: claudeMd, ecosystems }
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/analysis/classifiers/onboarding.ts
|
|
1107
|
+
function classifyOnboarding(_summary, snapshot, _config) {
|
|
1108
|
+
const level = computeExperienceLevel(snapshot);
|
|
1109
|
+
const recommendations = [];
|
|
1110
|
+
let index = 0;
|
|
1111
|
+
if (level.tier === "newcomer") {
|
|
1112
|
+
if (level.breakdown.hooks === 0) {
|
|
1113
|
+
recommendations.push({
|
|
1114
|
+
id: `rec-onboarding-${index}`,
|
|
1115
|
+
target: "HOOK",
|
|
1116
|
+
confidence: "MEDIUM",
|
|
1117
|
+
pattern_type: "onboarding_start_hooks",
|
|
1118
|
+
title: "Start automating: create your first hook",
|
|
1119
|
+
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.",
|
|
1120
|
+
evidence: {
|
|
1121
|
+
count: 0,
|
|
1122
|
+
examples: ["No hooks detected in your environment"]
|
|
1123
|
+
},
|
|
1124
|
+
suggested_action: "Add a hook in .claude/settings.json hooks section for automation."
|
|
1125
|
+
});
|
|
1126
|
+
index++;
|
|
1127
|
+
}
|
|
1128
|
+
if (level.breakdown.rules === 0) {
|
|
1129
|
+
recommendations.push({
|
|
1130
|
+
id: `rec-onboarding-${index}`,
|
|
1131
|
+
target: "RULE",
|
|
1132
|
+
confidence: "MEDIUM",
|
|
1133
|
+
pattern_type: "onboarding_start_rules",
|
|
1134
|
+
title: "Define coding preferences: add your first rule",
|
|
1135
|
+
description: "Rules (.claude/rules/) codify conventions that Claude follows automatically. They persist across sessions and ensure consistent behavior.",
|
|
1136
|
+
evidence: {
|
|
1137
|
+
count: 0,
|
|
1138
|
+
examples: ["No rules detected in your environment"]
|
|
1139
|
+
},
|
|
1140
|
+
suggested_action: "Create .claude/rules/ directory with a rule for your preferred coding style."
|
|
1141
|
+
});
|
|
1142
|
+
index++;
|
|
1143
|
+
}
|
|
1144
|
+
if (level.breakdown.claude_md === 0) {
|
|
1145
|
+
recommendations.push({
|
|
1146
|
+
id: `rec-onboarding-${index}`,
|
|
1147
|
+
target: "CLAUDE_MD",
|
|
1148
|
+
confidence: "MEDIUM",
|
|
1149
|
+
pattern_type: "onboarding_start_claudemd",
|
|
1150
|
+
title: "Set project context: create CLAUDE.md",
|
|
1151
|
+
description: "CLAUDE.md gives Claude project-specific context \u2014 tech stack, conventions, and constraints. It is loaded automatically at the start of every conversation.",
|
|
1152
|
+
evidence: {
|
|
1153
|
+
count: 0,
|
|
1154
|
+
examples: ["No CLAUDE.md files detected in your environment"]
|
|
1155
|
+
},
|
|
1156
|
+
suggested_action: "Create CLAUDE.md in your project root with project description and conventions."
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
} else if (level.tier === "power_user") {
|
|
1160
|
+
recommendations.push({
|
|
1161
|
+
id: "rec-onboarding-3",
|
|
1162
|
+
target: "SETTINGS",
|
|
1163
|
+
confidence: "LOW",
|
|
1164
|
+
pattern_type: "onboarding_optimize",
|
|
1165
|
+
title: "Consider mechanizing recurring patterns",
|
|
1166
|
+
description: "Your extensive configuration suggests active automation investment. Review for redundancy or upgrade opportunities \u2014 hooks and rules with overlapping concerns can be consolidated.",
|
|
1167
|
+
evidence: {
|
|
1168
|
+
count: level.score,
|
|
1169
|
+
examples: [
|
|
1170
|
+
`${level.breakdown.hooks} hooks, ${level.breakdown.rules} rules, ${level.breakdown.plugins} plugins installed`
|
|
1171
|
+
]
|
|
1172
|
+
},
|
|
1173
|
+
suggested_action: "Review your hooks and rules for overlapping concerns that could be consolidated."
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return recommendations;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/analysis/classifiers/index.ts
|
|
1180
|
+
var classifiers = [
|
|
1181
|
+
classifyRepeatedPrompts,
|
|
1182
|
+
classifyLongPrompts,
|
|
1183
|
+
classifyPermissionPatterns,
|
|
1184
|
+
classifyCodeCorrections,
|
|
1185
|
+
classifyPersonalInfo,
|
|
1186
|
+
classifyConfigDrift,
|
|
1187
|
+
classifyEcosystemAdaptations,
|
|
1188
|
+
classifyOnboarding
|
|
1189
|
+
];
|
|
1190
|
+
|
|
1191
|
+
// src/schemas/recommendation.ts
|
|
1192
|
+
import { z as z6 } from "zod/v4";
|
|
1193
|
+
var routingTargetSchema = z6.enum([
|
|
1194
|
+
"HOOK",
|
|
1195
|
+
"SKILL",
|
|
1196
|
+
"RULE",
|
|
1197
|
+
"CLAUDE_MD",
|
|
1198
|
+
"MEMORY",
|
|
1199
|
+
"SETTINGS"
|
|
1200
|
+
]);
|
|
1201
|
+
var confidenceSchema = z6.enum(["HIGH", "MEDIUM", "LOW"]);
|
|
1202
|
+
var patternTypeSchema = z6.enum([
|
|
1203
|
+
"repeated_prompt",
|
|
1204
|
+
"long_prompt",
|
|
1205
|
+
"permission-always-approved",
|
|
1206
|
+
"code_correction",
|
|
1207
|
+
"personal_info",
|
|
1208
|
+
"config_drift",
|
|
1209
|
+
"version_update",
|
|
1210
|
+
"ecosystem_gsd",
|
|
1211
|
+
"ecosystem_cog",
|
|
1212
|
+
"onboarding_start_hooks",
|
|
1213
|
+
"onboarding_start_rules",
|
|
1214
|
+
"onboarding_start_claudemd",
|
|
1215
|
+
"onboarding_optimize",
|
|
1216
|
+
"scan_redundancy",
|
|
1217
|
+
"scan_missing_mechanization",
|
|
1218
|
+
"scan_stale_reference"
|
|
1219
|
+
]);
|
|
1220
|
+
var recommendationSchema = z6.object({
|
|
1221
|
+
id: z6.string(),
|
|
1222
|
+
target: routingTargetSchema,
|
|
1223
|
+
confidence: confidenceSchema,
|
|
1224
|
+
pattern_type: patternTypeSchema,
|
|
1225
|
+
title: z6.string(),
|
|
1226
|
+
description: z6.string(),
|
|
1227
|
+
evidence: z6.object({
|
|
1228
|
+
count: z6.number(),
|
|
1229
|
+
sessions: z6.number().optional(),
|
|
1230
|
+
examples: z6.array(z6.string()).max(3)
|
|
1231
|
+
}),
|
|
1232
|
+
suggested_action: z6.string(),
|
|
1233
|
+
ecosystem_context: z6.string().optional()
|
|
1234
|
+
});
|
|
1235
|
+
var DEFAULT_THRESHOLDS = {
|
|
1236
|
+
repeated_prompt_min_count: 5,
|
|
1237
|
+
repeated_prompt_high_count: 10,
|
|
1238
|
+
repeated_prompt_high_sessions: 3,
|
|
1239
|
+
repeated_prompt_medium_sessions: 2,
|
|
1240
|
+
long_prompt_min_words: 200,
|
|
1241
|
+
long_prompt_min_count: 2,
|
|
1242
|
+
long_prompt_high_words: 300,
|
|
1243
|
+
long_prompt_high_count: 3,
|
|
1244
|
+
permission_approval_min_count: 10,
|
|
1245
|
+
permission_approval_min_sessions: 3,
|
|
1246
|
+
permission_approval_high_count: 15,
|
|
1247
|
+
permission_approval_high_sessions: 4,
|
|
1248
|
+
code_correction_min_failure_rate: 0.3,
|
|
1249
|
+
code_correction_min_failures: 3
|
|
1250
|
+
};
|
|
1251
|
+
var analysisConfigSchema = z6.object({
|
|
1252
|
+
thresholds: z6.object({
|
|
1253
|
+
repeated_prompt_min_count: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_min_count),
|
|
1254
|
+
repeated_prompt_high_count: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_count),
|
|
1255
|
+
repeated_prompt_high_sessions: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_sessions),
|
|
1256
|
+
repeated_prompt_medium_sessions: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_medium_sessions),
|
|
1257
|
+
long_prompt_min_words: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_min_words),
|
|
1258
|
+
long_prompt_min_count: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_min_count),
|
|
1259
|
+
long_prompt_high_words: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_high_words),
|
|
1260
|
+
long_prompt_high_count: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_high_count),
|
|
1261
|
+
permission_approval_min_count: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_min_count),
|
|
1262
|
+
permission_approval_min_sessions: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_min_sessions),
|
|
1263
|
+
permission_approval_high_count: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_high_count),
|
|
1264
|
+
permission_approval_high_sessions: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_high_sessions),
|
|
1265
|
+
code_correction_min_failure_rate: z6.number().default(DEFAULT_THRESHOLDS.code_correction_min_failure_rate),
|
|
1266
|
+
code_correction_min_failures: z6.number().default(DEFAULT_THRESHOLDS.code_correction_min_failures)
|
|
1267
|
+
}).default(() => ({ ...DEFAULT_THRESHOLDS })),
|
|
1268
|
+
max_recommendations: z6.number().default(20)
|
|
1269
|
+
}).default(() => ({
|
|
1270
|
+
thresholds: { ...DEFAULT_THRESHOLDS },
|
|
1271
|
+
max_recommendations: 20
|
|
1272
|
+
}));
|
|
1273
|
+
var analysisResultSchema = z6.object({
|
|
1274
|
+
generated_at: z6.iso.datetime(),
|
|
1275
|
+
summary_period: z6.object({
|
|
1276
|
+
since: z6.string(),
|
|
1277
|
+
until: z6.string(),
|
|
1278
|
+
days: z6.number()
|
|
1279
|
+
}),
|
|
1280
|
+
recommendations: z6.array(recommendationSchema),
|
|
1281
|
+
metadata: z6.object({
|
|
1282
|
+
classifier_count: z6.number(),
|
|
1283
|
+
patterns_evaluated: z6.number(),
|
|
1284
|
+
environment_ecosystems: z6.array(z6.string()),
|
|
1285
|
+
claude_code_version: z6.string()
|
|
1286
|
+
})
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// src/analysis/analyzer.ts
|
|
1290
|
+
var CONFIDENCE_ORDER = {
|
|
1291
|
+
HIGH: 0,
|
|
1292
|
+
MEDIUM: 1,
|
|
1293
|
+
LOW: 2
|
|
1294
|
+
};
|
|
1295
|
+
function sortRecommendations(a, b) {
|
|
1296
|
+
const confDiff = (CONFIDENCE_ORDER[a.confidence] ?? 3) - (CONFIDENCE_ORDER[b.confidence] ?? 3);
|
|
1297
|
+
if (confDiff !== 0) return confDiff;
|
|
1298
|
+
return b.evidence.count - a.evidence.count;
|
|
1299
|
+
}
|
|
1300
|
+
function adjustConfidence(recommendations, summaries) {
|
|
1301
|
+
const rateByType = new Map(
|
|
1302
|
+
summaries.map((s) => [s.pattern_type, s.persistence_rate])
|
|
1303
|
+
);
|
|
1304
|
+
return recommendations.map((rec) => {
|
|
1305
|
+
const rate = rateByType.get(rec.pattern_type);
|
|
1306
|
+
if (rate === void 0 || rate >= 0.7) return rec;
|
|
1307
|
+
const downgraded = {
|
|
1308
|
+
HIGH: "MEDIUM",
|
|
1309
|
+
MEDIUM: "LOW",
|
|
1310
|
+
LOW: "LOW"
|
|
1311
|
+
};
|
|
1312
|
+
return {
|
|
1313
|
+
...rec,
|
|
1314
|
+
confidence: downgraded[rec.confidence] ?? rec.confidence
|
|
1315
|
+
};
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
function analyze(summary, snapshot, config, outcomeSummaries) {
|
|
1319
|
+
const mergedConfig = config ?? analysisConfigSchema.parse({});
|
|
1320
|
+
const recommendations = [];
|
|
1321
|
+
for (const classify of classifiers) {
|
|
1322
|
+
const results = classify(summary, snapshot, mergedConfig);
|
|
1323
|
+
recommendations.push(...results);
|
|
1324
|
+
}
|
|
1325
|
+
const adjusted = outcomeSummaries && outcomeSummaries.length > 0 ? adjustConfidence(recommendations, outcomeSummaries) : recommendations;
|
|
1326
|
+
adjusted.sort(sortRecommendations);
|
|
1327
|
+
const capped = adjusted.slice(0, mergedConfig.max_recommendations);
|
|
1328
|
+
const patternsEvaluated = summary.top_repeated_prompts.length + summary.long_prompts.length + summary.permission_patterns.length + summary.tool_frequency.length;
|
|
1329
|
+
return analysisResultSchema.parse({
|
|
1330
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1331
|
+
summary_period: summary.period,
|
|
1332
|
+
recommendations: capped,
|
|
1333
|
+
metadata: {
|
|
1334
|
+
classifier_count: classifiers.length,
|
|
1335
|
+
patterns_evaluated: patternsEvaluated,
|
|
1336
|
+
environment_ecosystems: snapshot.detected_ecosystems,
|
|
1337
|
+
claude_code_version: snapshot.claude_code.version
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/analysis/outcome-tracker.ts
|
|
1343
|
+
import { readFile as readFile5, appendFile } from "fs/promises";
|
|
1344
|
+
|
|
1345
|
+
// src/delivery/state.ts
|
|
1346
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1347
|
+
import writeFileAtomic5 from "write-file-atomic";
|
|
1348
|
+
|
|
1349
|
+
// src/schemas/delivery.ts
|
|
1350
|
+
import { z as z7 } from "zod/v4";
|
|
1351
|
+
var recommendationStatusSchema = z7.enum(["pending", "applied", "dismissed"]);
|
|
1352
|
+
var recommendationStateEntrySchema = z7.object({
|
|
1353
|
+
id: z7.string(),
|
|
1354
|
+
status: recommendationStatusSchema,
|
|
1355
|
+
updated_at: z7.iso.datetime(),
|
|
1356
|
+
applied_details: z7.string().optional()
|
|
1357
|
+
});
|
|
1358
|
+
var recommendationStateSchema = z7.object({
|
|
1359
|
+
entries: z7.array(recommendationStateEntrySchema),
|
|
1360
|
+
last_updated: z7.iso.datetime()
|
|
1361
|
+
});
|
|
1362
|
+
var autoApplyLogEntrySchema = z7.object({
|
|
1363
|
+
timestamp: z7.iso.datetime(),
|
|
1364
|
+
recommendation_id: z7.string(),
|
|
1365
|
+
target: z7.string(),
|
|
1366
|
+
action: z7.string(),
|
|
1367
|
+
success: z7.boolean(),
|
|
1368
|
+
details: z7.string().optional(),
|
|
1369
|
+
backup_path: z7.string().optional()
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
// src/delivery/state.ts
|
|
1373
|
+
async function loadState() {
|
|
1374
|
+
try {
|
|
1375
|
+
const raw = await readFile4(paths.recommendationState, "utf-8");
|
|
1376
|
+
return recommendationStateSchema.parse(JSON.parse(raw));
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
1379
|
+
return { entries: [], last_updated: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1380
|
+
}
|
|
1381
|
+
throw err;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
async function saveState(state) {
|
|
1385
|
+
await writeFileAtomic5(
|
|
1386
|
+
paths.recommendationState,
|
|
1387
|
+
JSON.stringify(state, null, 2)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
async function updateStatus(id, status, details) {
|
|
1391
|
+
const state = await loadState();
|
|
1392
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1393
|
+
const existing = state.entries.find((e) => e.id === id);
|
|
1394
|
+
if (existing) {
|
|
1395
|
+
existing.status = status;
|
|
1396
|
+
existing.updated_at = now;
|
|
1397
|
+
if (status === "applied" && details !== void 0) {
|
|
1398
|
+
existing.applied_details = details;
|
|
1399
|
+
} else if (status !== "applied") {
|
|
1400
|
+
existing.applied_details = void 0;
|
|
1401
|
+
}
|
|
1402
|
+
} else {
|
|
1403
|
+
state.entries.push({
|
|
1404
|
+
id,
|
|
1405
|
+
status,
|
|
1406
|
+
updated_at: now,
|
|
1407
|
+
...status === "applied" && details !== void 0 ? { applied_details: details } : {}
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
state.last_updated = now;
|
|
1411
|
+
await saveState(state);
|
|
1412
|
+
}
|
|
1413
|
+
async function getStatusMap() {
|
|
1414
|
+
const state = await loadState();
|
|
1415
|
+
return new Map(state.entries.map((e) => [e.id, e.status]));
|
|
1416
|
+
}
|
|
1417
|
+
function isNodeError(err) {
|
|
1418
|
+
return err instanceof Error && "code" in err;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/schemas/onboarding.ts
|
|
1422
|
+
import { z as z8 } from "zod/v4";
|
|
1423
|
+
var experienceTierSchema = z8.enum(["newcomer", "intermediate", "power_user"]);
|
|
1424
|
+
var experienceLevelSchema = z8.object({
|
|
1425
|
+
tier: experienceTierSchema,
|
|
1426
|
+
score: z8.number().min(0).max(100),
|
|
1427
|
+
breakdown: z8.object({
|
|
1428
|
+
hooks: z8.number(),
|
|
1429
|
+
rules: z8.number(),
|
|
1430
|
+
skills: z8.number(),
|
|
1431
|
+
plugins: z8.number(),
|
|
1432
|
+
claude_md: z8.number(),
|
|
1433
|
+
ecosystems: z8.number()
|
|
1434
|
+
})
|
|
1435
|
+
});
|
|
1436
|
+
var outcomeEntrySchema = z8.object({
|
|
1437
|
+
recommendation_id: z8.string(),
|
|
1438
|
+
pattern_type: z8.string(),
|
|
1439
|
+
target: z8.string(),
|
|
1440
|
+
applied_at: z8.iso.datetime(),
|
|
1441
|
+
checked_at: z8.iso.datetime(),
|
|
1442
|
+
persisted: z8.boolean(),
|
|
1443
|
+
checks_since_applied: z8.number(),
|
|
1444
|
+
outcome: z8.enum(["positive", "negative", "monitoring"])
|
|
1445
|
+
});
|
|
1446
|
+
var outcomeSummarySchema = z8.object({
|
|
1447
|
+
pattern_type: z8.string(),
|
|
1448
|
+
total_applied: z8.number(),
|
|
1449
|
+
total_persisted: z8.number(),
|
|
1450
|
+
total_reverted: z8.number(),
|
|
1451
|
+
persistence_rate: z8.number()
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// src/analysis/outcome-tracker.ts
|
|
1455
|
+
async function trackOutcomes(snapshot) {
|
|
1456
|
+
const state = await loadState();
|
|
1457
|
+
const applied = state.entries.filter((e) => e.status === "applied");
|
|
1458
|
+
if (applied.length === 0) return [];
|
|
1459
|
+
const history = await loadOutcomeHistory();
|
|
1460
|
+
const results = [];
|
|
1461
|
+
for (const entry of applied) {
|
|
1462
|
+
const priorEntries = history.filter(
|
|
1463
|
+
(h) => h.recommendation_id === entry.id
|
|
1464
|
+
);
|
|
1465
|
+
const latest = priorEntries.length > 0 ? priorEntries[priorEntries.length - 1] : void 0;
|
|
1466
|
+
const checksCount = latest ? latest.checks_since_applied + 1 : 1;
|
|
1467
|
+
const persisted = checkPersistence(entry, snapshot);
|
|
1468
|
+
let outcome;
|
|
1469
|
+
if (!persisted) {
|
|
1470
|
+
outcome = "negative";
|
|
1471
|
+
} else if (checksCount >= 5) {
|
|
1472
|
+
outcome = "positive";
|
|
1473
|
+
} else {
|
|
1474
|
+
outcome = "monitoring";
|
|
1475
|
+
}
|
|
1476
|
+
const patternType = inferPatternType(entry.id);
|
|
1477
|
+
const target = inferTarget(entry.id);
|
|
1478
|
+
const outcomeEntry = {
|
|
1479
|
+
recommendation_id: entry.id,
|
|
1480
|
+
pattern_type: patternType,
|
|
1481
|
+
target,
|
|
1482
|
+
applied_at: entry.updated_at,
|
|
1483
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1484
|
+
persisted,
|
|
1485
|
+
checks_since_applied: checksCount,
|
|
1486
|
+
outcome
|
|
1487
|
+
};
|
|
1488
|
+
results.push(outcomeEntry);
|
|
1489
|
+
await appendOutcome(outcomeEntry);
|
|
1490
|
+
}
|
|
1491
|
+
return results;
|
|
1492
|
+
}
|
|
1493
|
+
function checkPersistence(entry, snapshot) {
|
|
1494
|
+
if (entry.applied_details) {
|
|
1495
|
+
const toolMatch = entry.applied_details.match(/Added (\w+) to allowedTools/);
|
|
1496
|
+
if (toolMatch) {
|
|
1497
|
+
const toolName = toolMatch[1];
|
|
1498
|
+
const userSettings = snapshot.settings.user;
|
|
1499
|
+
if (!userSettings) return false;
|
|
1500
|
+
const allowedTools = userSettings.allowedTools;
|
|
1501
|
+
if (!Array.isArray(allowedTools)) return false;
|
|
1502
|
+
return allowedTools.includes(toolName);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (entry.id.startsWith("rec-repeated-")) {
|
|
1506
|
+
return snapshot.installed_tools.hooks.length > 0;
|
|
1507
|
+
}
|
|
1508
|
+
if (entry.id.startsWith("rec-long-")) {
|
|
1509
|
+
return snapshot.installed_tools.skills.length > 0;
|
|
1510
|
+
}
|
|
1511
|
+
if (entry.id.startsWith("rec-correction-")) {
|
|
1512
|
+
return snapshot.installed_tools.rules.length > 0;
|
|
1513
|
+
}
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
async function appendOutcome(entry) {
|
|
1517
|
+
await appendFile(
|
|
1518
|
+
paths.outcomeHistory,
|
|
1519
|
+
JSON.stringify(entry) + "\n",
|
|
1520
|
+
"utf-8"
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
async function loadOutcomeHistory() {
|
|
1524
|
+
let raw;
|
|
1525
|
+
try {
|
|
1526
|
+
raw = await readFile5(paths.outcomeHistory, "utf-8");
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
if (isNodeError2(err) && err.code === "ENOENT") {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
throw err;
|
|
1532
|
+
}
|
|
1533
|
+
const entries = [];
|
|
1534
|
+
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
1535
|
+
for (const line of lines) {
|
|
1536
|
+
try {
|
|
1537
|
+
const parsed = JSON.parse(line);
|
|
1538
|
+
const result = outcomeEntrySchema.safeParse(parsed);
|
|
1539
|
+
if (result.success) {
|
|
1540
|
+
entries.push(result.data);
|
|
1541
|
+
}
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return entries;
|
|
1546
|
+
}
|
|
1547
|
+
function computeOutcomeSummaries(history) {
|
|
1548
|
+
if (history.length === 0) return [];
|
|
1549
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const entry of history) {
|
|
1551
|
+
const group = groups.get(entry.pattern_type) ?? [];
|
|
1552
|
+
group.push(entry);
|
|
1553
|
+
groups.set(entry.pattern_type, group);
|
|
1554
|
+
}
|
|
1555
|
+
const summaries = [];
|
|
1556
|
+
for (const [patternType, entries] of groups) {
|
|
1557
|
+
const latestByRec = /* @__PURE__ */ new Map();
|
|
1558
|
+
for (const entry of entries) {
|
|
1559
|
+
latestByRec.set(entry.recommendation_id, entry);
|
|
1560
|
+
}
|
|
1561
|
+
let totalPersisted = 0;
|
|
1562
|
+
let totalReverted = 0;
|
|
1563
|
+
for (const entry of latestByRec.values()) {
|
|
1564
|
+
if (entry.outcome === "positive") {
|
|
1565
|
+
totalPersisted++;
|
|
1566
|
+
} else if (entry.outcome === "negative") {
|
|
1567
|
+
totalReverted++;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const totalApplied = latestByRec.size;
|
|
1571
|
+
const persistenceRate = totalApplied > 0 ? totalPersisted / totalApplied : 0;
|
|
1572
|
+
summaries.push({
|
|
1573
|
+
pattern_type: patternType,
|
|
1574
|
+
total_applied: totalApplied,
|
|
1575
|
+
total_persisted: totalPersisted,
|
|
1576
|
+
total_reverted: totalReverted,
|
|
1577
|
+
persistence_rate: persistenceRate
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
return summaries;
|
|
1581
|
+
}
|
|
1582
|
+
function inferPatternType(id) {
|
|
1583
|
+
if (id.startsWith("rec-repeated-")) return "repeated_prompt";
|
|
1584
|
+
if (id.startsWith("rec-long-")) return "long_prompt";
|
|
1585
|
+
if (id.startsWith("rec-permission-always-approved-")) return "permission-always-approved";
|
|
1586
|
+
if (id.startsWith("rec-correction-")) return "code_correction";
|
|
1587
|
+
if (id.startsWith("rec-personal-")) return "personal_info";
|
|
1588
|
+
if (id.startsWith("rec-drift-")) return "config_drift";
|
|
1589
|
+
if (id.startsWith("rec-ecosystem-")) return "version_update";
|
|
1590
|
+
if (id.startsWith("rec-onboarding-")) return "onboarding_start_hooks";
|
|
1591
|
+
if (id.startsWith("rec-tool-preference-")) return "tool-preference";
|
|
1592
|
+
return "unknown";
|
|
1593
|
+
}
|
|
1594
|
+
function inferTarget(id) {
|
|
1595
|
+
if (id.startsWith("rec-repeated-")) return "HOOK";
|
|
1596
|
+
if (id.startsWith("rec-long-")) return "SKILL";
|
|
1597
|
+
if (id.startsWith("rec-permission-always-approved-")) return "SETTINGS";
|
|
1598
|
+
if (id.startsWith("rec-correction-")) return "RULE";
|
|
1599
|
+
if (id.startsWith("rec-personal-")) return "MEMORY";
|
|
1600
|
+
if (id.startsWith("rec-drift-")) return "CLAUDE_MD";
|
|
1601
|
+
if (id.startsWith("rec-ecosystem-")) return "CLAUDE_MD";
|
|
1602
|
+
if (id.startsWith("rec-tool-preference-")) return "SETTINGS";
|
|
1603
|
+
if (id.startsWith("rec-onboarding-")) return "HOOK";
|
|
1604
|
+
return "MEMORY";
|
|
1605
|
+
}
|
|
1606
|
+
function isNodeError2(err) {
|
|
1607
|
+
return err instanceof Error && "code" in err;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/analysis/trigger.ts
|
|
1611
|
+
import writeFileAtomic6 from "write-file-atomic";
|
|
1612
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1613
|
+
import { lock as lock2 } from "proper-lockfile";
|
|
1614
|
+
var COOLDOWN_MS = 6e4;
|
|
1615
|
+
async function writeAnalysisResult(result) {
|
|
1616
|
+
await ensureInit();
|
|
1617
|
+
await writeFileAtomic6(paths.analysisResult, JSON.stringify(result, null, 2));
|
|
1618
|
+
}
|
|
1619
|
+
async function runAnalysis(cwd) {
|
|
1620
|
+
const summary = await preProcess();
|
|
1621
|
+
const snapshot = await scanEnvironment(cwd);
|
|
1622
|
+
let outcomeSummaries;
|
|
1623
|
+
try {
|
|
1624
|
+
await trackOutcomes(snapshot);
|
|
1625
|
+
const history = await loadOutcomeHistory();
|
|
1626
|
+
outcomeSummaries = computeOutcomeSummaries(history);
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1629
|
+
const result = analyze(summary, snapshot, void 0, outcomeSummaries);
|
|
1630
|
+
await writeAnalysisResult(result);
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
async function resetCounterWithTimestamp() {
|
|
1634
|
+
try {
|
|
1635
|
+
await readFile6(paths.counter, "utf-8");
|
|
1636
|
+
} catch {
|
|
1637
|
+
const initial = {
|
|
1638
|
+
total: 0,
|
|
1639
|
+
session: {},
|
|
1640
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1641
|
+
};
|
|
1642
|
+
await writeFileAtomic6(paths.counter, JSON.stringify(initial, null, 2));
|
|
1643
|
+
}
|
|
1644
|
+
const release = await lock2(paths.counter, {
|
|
1645
|
+
retries: { retries: 50, minTimeout: 20, maxTimeout: 1e3, randomize: true },
|
|
1646
|
+
stale: 1e4
|
|
1647
|
+
});
|
|
1648
|
+
try {
|
|
1649
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1650
|
+
const data = {
|
|
1651
|
+
total: 0,
|
|
1652
|
+
session: {},
|
|
1653
|
+
last_analysis: now,
|
|
1654
|
+
last_updated: now
|
|
1655
|
+
};
|
|
1656
|
+
await writeFileAtomic6(paths.counter, JSON.stringify(data, null, 2));
|
|
1657
|
+
} finally {
|
|
1658
|
+
await release();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async function checkAndTriggerAnalysis(cwd) {
|
|
1662
|
+
const config = await loadConfig();
|
|
1663
|
+
if (!config.analysis.enabled) return false;
|
|
1664
|
+
const counter = await readCounter();
|
|
1665
|
+
if (counter.total < config.analysis.threshold) return false;
|
|
1666
|
+
if (counter.last_analysis) {
|
|
1667
|
+
const elapsed = Date.now() - new Date(counter.last_analysis).getTime();
|
|
1668
|
+
if (elapsed < COOLDOWN_MS) return false;
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
await runAnalysis(cwd);
|
|
1672
|
+
} catch {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
await resetCounterWithTimestamp();
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/hooks/stop.ts
|
|
1680
|
+
async function handleStop(rawJson) {
|
|
1681
|
+
try {
|
|
1682
|
+
const input = stopInputSchema.parse(JSON.parse(rawJson));
|
|
1683
|
+
if (input.stop_hook_active) return;
|
|
1684
|
+
await checkAndTriggerAnalysis(input.cwd);
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
async function main() {
|
|
1689
|
+
try {
|
|
1690
|
+
const raw = await readStdin();
|
|
1691
|
+
await handleStop(raw);
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
process.exit(0);
|
|
1695
|
+
}
|
|
1696
|
+
main();
|
|
1697
|
+
|
|
1698
|
+
// src/scrubber/patterns.ts
|
|
1699
|
+
var SCRUB_PATTERNS = [
|
|
1700
|
+
{
|
|
1701
|
+
name: "aws_key",
|
|
1702
|
+
regex: /AKIA[0-9A-Z]{16}/g,
|
|
1703
|
+
replacement: "[REDACTED:aws_key]"
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
name: "aws_secret",
|
|
1707
|
+
regex: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)[\s=:]+[A-Za-z0-9/+=]{40}/g,
|
|
1708
|
+
replacement: "[REDACTED:aws_secret]"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
name: "github_token",
|
|
1712
|
+
regex: /gh[ps]_[A-Za-z0-9_]{36,}/g,
|
|
1713
|
+
replacement: "[REDACTED:github_token]"
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
name: "github_oauth",
|
|
1717
|
+
regex: /gho_[A-Za-z0-9_]{36,}/g,
|
|
1718
|
+
replacement: "[REDACTED:github_oauth]"
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
name: "bearer_token",
|
|
1722
|
+
regex: /[Bb]earer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
1723
|
+
replacement: "[REDACTED:bearer_token]"
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
name: "jwt",
|
|
1727
|
+
regex: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g,
|
|
1728
|
+
replacement: "[REDACTED:jwt]"
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
name: "api_key",
|
|
1732
|
+
regex: /(?:api[_-]?key|apikey)[\s=:]+['"]?[A-Za-z0-9\-._]{20,}['"]?/gi,
|
|
1733
|
+
replacement: "[REDACTED:api_key]"
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
name: "secret",
|
|
1737
|
+
regex: /(?:secret|SECRET)[\s=:]+['"]?[A-Za-z0-9\-._]{20,}['"]?/g,
|
|
1738
|
+
replacement: "[REDACTED:secret]"
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
name: "private_key",
|
|
1742
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g,
|
|
1743
|
+
replacement: "[REDACTED:private_key]"
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
name: "password",
|
|
1747
|
+
regex: /(?:password|passwd|pwd)[\s=:]+['"]?[^\s'"]{8,}['"]?/gi,
|
|
1748
|
+
replacement: "[REDACTED:password]"
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: "slack_token",
|
|
1752
|
+
regex: /xox[bpors]-[A-Za-z0-9-]{10,}/g,
|
|
1753
|
+
replacement: "[REDACTED:slack_token]"
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: "google_api_key",
|
|
1757
|
+
regex: /AIza[0-9A-Za-z\-_]{35}/g,
|
|
1758
|
+
replacement: "[REDACTED:google_api_key]"
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
name: "stripe_key",
|
|
1762
|
+
regex: /[sr]k_(?:test|live)_[A-Za-z0-9]{20,}/g,
|
|
1763
|
+
replacement: "[REDACTED:stripe_key]"
|
|
1764
|
+
},
|
|
1765
|
+
{
|
|
1766
|
+
name: "db_url",
|
|
1767
|
+
regex: /(?:postgres|mysql|mongodb):\/\/[^\s]+:[^\s]+@[^\s]+/g,
|
|
1768
|
+
replacement: "[REDACTED:db_url]"
|
|
1769
|
+
}
|
|
1770
|
+
// Pattern 15: High-entropy string detection is intentionally omitted.
|
|
1771
|
+
// It requires Shannon entropy calculation and is deferred to when
|
|
1772
|
+
// config.scrubbing.highEntropyDetection is enabled.
|
|
1773
|
+
];
|
|
1774
|
+
|
|
1775
|
+
// src/scrubber/scrub.ts
|
|
1776
|
+
function scrubString(input, extraPatterns) {
|
|
1777
|
+
let result = input;
|
|
1778
|
+
const patterns = extraPatterns ? [...SCRUB_PATTERNS, ...extraPatterns] : SCRUB_PATTERNS;
|
|
1779
|
+
for (const pattern of patterns) {
|
|
1780
|
+
pattern.regex.lastIndex = 0;
|
|
1781
|
+
result = result.replace(pattern.regex, pattern.replacement);
|
|
1782
|
+
}
|
|
1783
|
+
return result;
|
|
1784
|
+
}
|
|
1785
|
+
function scrubObject(obj, extraPatterns) {
|
|
1786
|
+
if (typeof obj === "string") {
|
|
1787
|
+
return scrubString(obj, extraPatterns);
|
|
1788
|
+
}
|
|
1789
|
+
if (Array.isArray(obj)) {
|
|
1790
|
+
return obj.map((item) => scrubObject(item, extraPatterns));
|
|
1791
|
+
}
|
|
1792
|
+
if (obj !== null && typeof obj === "object") {
|
|
1793
|
+
const result = {};
|
|
1794
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1795
|
+
result[key] = scrubObject(value, extraPatterns);
|
|
1796
|
+
}
|
|
1797
|
+
return result;
|
|
1798
|
+
}
|
|
1799
|
+
return obj;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// src/storage/logger.ts
|
|
1803
|
+
import { appendFile as appendFile2 } from "fs/promises";
|
|
1804
|
+
import { join as join4 } from "path";
|
|
1805
|
+
var SCHEMA_MAP = {
|
|
1806
|
+
prompts: promptEntrySchema,
|
|
1807
|
+
tools: toolEntrySchema,
|
|
1808
|
+
permissions: permissionEntrySchema,
|
|
1809
|
+
sessions: sessionEntrySchema
|
|
1810
|
+
};
|
|
1811
|
+
function getLogDir(type) {
|
|
1812
|
+
return paths.logs[type];
|
|
1813
|
+
}
|
|
1814
|
+
function getLogFilePath(type, date) {
|
|
1815
|
+
const dateStr = (date ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1816
|
+
return join4(getLogDir(type), `${dateStr}.jsonl`);
|
|
1817
|
+
}
|
|
1818
|
+
async function appendLogEntry(type, rawEntry) {
|
|
1819
|
+
await ensureInit();
|
|
1820
|
+
const schema = SCHEMA_MAP[type];
|
|
1821
|
+
const validated = schema.parse(rawEntry);
|
|
1822
|
+
const scrubbed = scrubObject(validated);
|
|
1823
|
+
const line = JSON.stringify(scrubbed) + "\n";
|
|
1824
|
+
const filePath = getLogFilePath(type);
|
|
1825
|
+
await appendFile2(filePath, line, "utf-8");
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/delivery/renderer.ts
|
|
1829
|
+
var TIER_ORDER = ["HIGH", "MEDIUM", "LOW"];
|
|
1830
|
+
function renderRecommendations(result, states) {
|
|
1831
|
+
const lines = [];
|
|
1832
|
+
lines.push("# harness-evolve Recommendations");
|
|
1833
|
+
lines.push("");
|
|
1834
|
+
lines.push(`*Generated: ${result.generated_at}*`);
|
|
1835
|
+
lines.push(
|
|
1836
|
+
`*Period: ${result.summary_period.since} to ${result.summary_period.until} (${result.summary_period.days} days)*`
|
|
1837
|
+
);
|
|
1838
|
+
lines.push("");
|
|
1839
|
+
if (result.recommendations.length === 0) {
|
|
1840
|
+
lines.push("No recommendations at this time.");
|
|
1841
|
+
lines.push("");
|
|
1842
|
+
lines.push("---");
|
|
1843
|
+
lines.push("*Run /evolve to refresh or manage recommendations.*");
|
|
1844
|
+
return lines.join("\n");
|
|
1845
|
+
}
|
|
1846
|
+
for (const tier of TIER_ORDER) {
|
|
1847
|
+
const tierRecs = result.recommendations.filter(
|
|
1848
|
+
(r) => r.confidence === tier
|
|
1849
|
+
);
|
|
1850
|
+
if (tierRecs.length === 0) continue;
|
|
1851
|
+
lines.push(`## ${tier} Confidence`);
|
|
1852
|
+
lines.push("");
|
|
1853
|
+
for (const rec of tierRecs) {
|
|
1854
|
+
const status = (states.get(rec.id) ?? "pending").toUpperCase();
|
|
1855
|
+
lines.push(`### [${status}] ${rec.title}`);
|
|
1856
|
+
lines.push("");
|
|
1857
|
+
lines.push(`**Target:** ${rec.target} | **Pattern:** ${rec.pattern_type}`);
|
|
1858
|
+
const evidenceParts = [`${rec.evidence.count} occurrences`];
|
|
1859
|
+
if (rec.evidence.sessions !== void 0) {
|
|
1860
|
+
evidenceParts.push(`across ${rec.evidence.sessions} sessions`);
|
|
1861
|
+
}
|
|
1862
|
+
lines.push(`**Evidence:** ${evidenceParts.join(" ")}`);
|
|
1863
|
+
lines.push("");
|
|
1864
|
+
lines.push(rec.description);
|
|
1865
|
+
lines.push("");
|
|
1866
|
+
if (rec.evidence.examples.length > 0) {
|
|
1867
|
+
lines.push("**Examples:**");
|
|
1868
|
+
for (const ex of rec.evidence.examples) {
|
|
1869
|
+
lines.push(`- \`${ex}\``);
|
|
1870
|
+
}
|
|
1871
|
+
lines.push("");
|
|
1872
|
+
}
|
|
1873
|
+
lines.push(`**Suggested action:** ${rec.suggested_action}`);
|
|
1874
|
+
lines.push("");
|
|
1875
|
+
if (rec.ecosystem_context !== void 0) {
|
|
1876
|
+
lines.push(`**Ecosystem note:** ${rec.ecosystem_context}`);
|
|
1877
|
+
lines.push("");
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
lines.push("---");
|
|
1882
|
+
lines.push("*Run /evolve to refresh or manage recommendations.*");
|
|
1883
|
+
return lines.join("\n");
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// src/delivery/rotator.ts
|
|
1887
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
1888
|
+
import { join as join5 } from "path";
|
|
1889
|
+
import writeFileAtomic7 from "write-file-atomic";
|
|
1890
|
+
async function rotateRecommendations(config) {
|
|
1891
|
+
const state = await loadState();
|
|
1892
|
+
const cutoff = new Date(Date.now() - config.archiveAfterDays * 864e5);
|
|
1893
|
+
const toArchive = state.entries.filter(
|
|
1894
|
+
(e) => (e.status === "applied" || e.status === "dismissed") && new Date(e.updated_at) < cutoff
|
|
1895
|
+
);
|
|
1896
|
+
if (toArchive.length === 0) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const toKeep = state.entries.filter(
|
|
1900
|
+
(e) => !toArchive.some((a) => a.id === e.id)
|
|
1901
|
+
);
|
|
1902
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1903
|
+
const archivePath = join5(paths.recommendationArchive, `${today}.json`);
|
|
1904
|
+
await mkdir2(paths.recommendationArchive, { recursive: true });
|
|
1905
|
+
await writeFileAtomic7(archivePath, JSON.stringify(toArchive, null, 2));
|
|
1906
|
+
await saveState({
|
|
1907
|
+
entries: toKeep,
|
|
1908
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/delivery/notification.ts
|
|
1913
|
+
import { existsSync } from "fs";
|
|
1914
|
+
import { readFile as readFile7, unlink, writeFile } from "fs/promises";
|
|
1915
|
+
function buildNotification(pendingCount) {
|
|
1916
|
+
const plural = pendingCount === 1 ? "" : "s";
|
|
1917
|
+
return `[harness-evolve] ${pendingCount} new suggestion${plural} found. Run /evolve:apply to review.`;
|
|
1918
|
+
}
|
|
1919
|
+
async function writeNotificationFlag(pendingCount) {
|
|
1920
|
+
await writeFile(paths.notificationFlag, String(pendingCount), "utf-8");
|
|
1921
|
+
}
|
|
1922
|
+
async function hasNotificationFlag() {
|
|
1923
|
+
return existsSync(paths.notificationFlag);
|
|
1924
|
+
}
|
|
1925
|
+
async function clearNotificationFlag() {
|
|
1926
|
+
try {
|
|
1927
|
+
await unlink(paths.notificationFlag);
|
|
1928
|
+
} catch {
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
async function readNotificationFlagCount() {
|
|
1932
|
+
try {
|
|
1933
|
+
const content = await readFile7(paths.notificationFlag, "utf-8");
|
|
1934
|
+
return parseInt(content.trim(), 10) || 0;
|
|
1935
|
+
} catch {
|
|
1936
|
+
return 0;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/delivery/auto-apply.ts
|
|
1941
|
+
import { appendFile as appendFile3 } from "fs/promises";
|
|
1942
|
+
|
|
1943
|
+
// src/delivery/appliers/index.ts
|
|
1944
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1945
|
+
function registerApplier(applier) {
|
|
1946
|
+
registry.set(applier.target, applier);
|
|
1947
|
+
}
|
|
1948
|
+
function getApplier(target) {
|
|
1949
|
+
return registry.get(target);
|
|
1950
|
+
}
|
|
1951
|
+
function hasApplier(target) {
|
|
1952
|
+
return registry.has(target);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/delivery/appliers/settings-applier.ts
|
|
1956
|
+
import { readFile as readFile8, copyFile, mkdir as mkdir3 } from "fs/promises";
|
|
1957
|
+
import { join as join6, dirname } from "path";
|
|
1958
|
+
import writeFileAtomic8 from "write-file-atomic";
|
|
1959
|
+
var SettingsApplier = class {
|
|
1960
|
+
target = "SETTINGS";
|
|
1961
|
+
canApply(rec) {
|
|
1962
|
+
return rec.confidence === "HIGH" && rec.target === "SETTINGS" && rec.pattern_type === "permission-always-approved";
|
|
1963
|
+
}
|
|
1964
|
+
async apply(rec, options) {
|
|
1965
|
+
try {
|
|
1966
|
+
if (rec.pattern_type !== "permission-always-approved") {
|
|
1967
|
+
return {
|
|
1968
|
+
recommendation_id: rec.id,
|
|
1969
|
+
success: false,
|
|
1970
|
+
details: `Skipped: pattern_type '${rec.pattern_type}' not supported for auto-apply in v1`
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
const settingsFilePath = options?.settingsPath ?? join6(process.env.HOME ?? "", ".claude", "settings.json");
|
|
1974
|
+
const raw = await readFile8(settingsFilePath, "utf-8");
|
|
1975
|
+
const settings = JSON.parse(raw);
|
|
1976
|
+
const backup = join6(
|
|
1977
|
+
paths.analysis,
|
|
1978
|
+
"backups",
|
|
1979
|
+
`settings-backup-${rec.id}.json`
|
|
1980
|
+
);
|
|
1981
|
+
await mkdir3(dirname(backup), { recursive: true });
|
|
1982
|
+
await copyFile(settingsFilePath, backup);
|
|
1983
|
+
const toolName = extractToolName(rec);
|
|
1984
|
+
if (!toolName) {
|
|
1985
|
+
return {
|
|
1986
|
+
recommendation_id: rec.id,
|
|
1987
|
+
success: false,
|
|
1988
|
+
details: "Could not extract tool name from recommendation evidence"
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
const allowedTools = Array.isArray(settings.allowedTools) ? settings.allowedTools : [];
|
|
1992
|
+
if (!allowedTools.includes(toolName)) {
|
|
1993
|
+
allowedTools.push(toolName);
|
|
1994
|
+
}
|
|
1995
|
+
settings.allowedTools = allowedTools;
|
|
1996
|
+
await writeFileAtomic8(
|
|
1997
|
+
settingsFilePath,
|
|
1998
|
+
JSON.stringify(settings, null, 2)
|
|
1999
|
+
);
|
|
2000
|
+
return {
|
|
2001
|
+
recommendation_id: rec.id,
|
|
2002
|
+
success: true,
|
|
2003
|
+
details: `Added ${toolName} to allowedTools`
|
|
2004
|
+
};
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2007
|
+
return {
|
|
2008
|
+
recommendation_id: rec.id,
|
|
2009
|
+
success: false,
|
|
2010
|
+
details: message
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
function extractToolName(rec) {
|
|
2016
|
+
for (const example of rec.evidence.examples) {
|
|
2017
|
+
const match = example.match(/^(\w+)\(/);
|
|
2018
|
+
if (match) return match[1];
|
|
2019
|
+
}
|
|
2020
|
+
return void 0;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/delivery/appliers/rule-applier.ts
|
|
2024
|
+
import { writeFile as writeFile2, access as access2, mkdir as mkdir4 } from "fs/promises";
|
|
2025
|
+
import { join as join7 } from "path";
|
|
2026
|
+
var RuleApplier = class {
|
|
2027
|
+
target = "RULE";
|
|
2028
|
+
canApply(rec) {
|
|
2029
|
+
return rec.confidence === "HIGH" && rec.target === "RULE";
|
|
2030
|
+
}
|
|
2031
|
+
async apply(rec, options) {
|
|
2032
|
+
const rulesDir = options?.rulesDir ?? join7(process.env.HOME ?? "", ".claude", "rules");
|
|
2033
|
+
const fileName = `evolve-${rec.pattern_type}.md`;
|
|
2034
|
+
const filePath = join7(rulesDir, fileName);
|
|
2035
|
+
try {
|
|
2036
|
+
await access2(filePath);
|
|
2037
|
+
return {
|
|
2038
|
+
recommendation_id: rec.id,
|
|
2039
|
+
success: false,
|
|
2040
|
+
details: `Rule file already exists: ${fileName}`
|
|
2041
|
+
};
|
|
2042
|
+
} catch {
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
await mkdir4(rulesDir, { recursive: true });
|
|
2046
|
+
const content = [
|
|
2047
|
+
`# ${rec.title}`,
|
|
2048
|
+
"",
|
|
2049
|
+
rec.description,
|
|
2050
|
+
"",
|
|
2051
|
+
"## Action",
|
|
2052
|
+
"",
|
|
2053
|
+
rec.suggested_action,
|
|
2054
|
+
"",
|
|
2055
|
+
"---",
|
|
2056
|
+
`*Auto-generated by harness-evolve (${rec.id})*`
|
|
2057
|
+
].join("\n");
|
|
2058
|
+
await writeFile2(filePath, content, "utf-8");
|
|
2059
|
+
return {
|
|
2060
|
+
recommendation_id: rec.id,
|
|
2061
|
+
success: true,
|
|
2062
|
+
details: `Created rule file: ${fileName}`
|
|
2063
|
+
};
|
|
2064
|
+
} catch (err) {
|
|
2065
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2066
|
+
return {
|
|
2067
|
+
recommendation_id: rec.id,
|
|
2068
|
+
success: false,
|
|
2069
|
+
details: message
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
// src/delivery/appliers/hook-applier.ts
|
|
2076
|
+
import { writeFile as writeFile3, access as access3, mkdir as mkdir5, chmod, copyFile as copyFile2 } from "fs/promises";
|
|
2077
|
+
import { join as join9, basename } from "path";
|
|
2078
|
+
|
|
2079
|
+
// src/generators/schemas.ts
|
|
2080
|
+
import { z as z9 } from "zod/v4";
|
|
2081
|
+
var GENERATOR_VERSION = "1.0.0";
|
|
2082
|
+
function nowISO() {
|
|
2083
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2084
|
+
}
|
|
2085
|
+
function toSlug(text) {
|
|
2086
|
+
if (!text) return "";
|
|
2087
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
2088
|
+
}
|
|
2089
|
+
var YAML_SPECIAL = /[:"'{}[\]#&*!|>\\,\n]/;
|
|
2090
|
+
function escapeYaml(text) {
|
|
2091
|
+
if (!YAML_SPECIAL.test(text)) return text;
|
|
2092
|
+
return `"${text.replace(/"/g, '\\"')}"`;
|
|
2093
|
+
}
|
|
2094
|
+
var generatedArtifactSchema = z9.object({
|
|
2095
|
+
type: z9.enum(["skill", "hook", "claude_md_patch"]),
|
|
2096
|
+
filename: z9.string(),
|
|
2097
|
+
content: z9.string(),
|
|
2098
|
+
source_recommendation_id: z9.string(),
|
|
2099
|
+
metadata: z9.object({
|
|
2100
|
+
generated_at: z9.iso.datetime(),
|
|
2101
|
+
generator_version: z9.string(),
|
|
2102
|
+
pattern_type: z9.string()
|
|
2103
|
+
})
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// src/generators/hook-generator.ts
|
|
2107
|
+
function extractHookEvent(rec) {
|
|
2108
|
+
const descMatch = rec.description.match(/suitable for a (\w+) hook/i);
|
|
2109
|
+
if (descMatch) return descMatch[1];
|
|
2110
|
+
const actionMatch = rec.suggested_action.match(/Create a (\w+) hook/i);
|
|
2111
|
+
if (actionMatch) return actionMatch[1];
|
|
2112
|
+
return "PreToolUse";
|
|
2113
|
+
}
|
|
2114
|
+
function generateHook(rec) {
|
|
2115
|
+
if (rec.target !== "HOOK") return null;
|
|
2116
|
+
const hookEvent = extractHookEvent(rec);
|
|
2117
|
+
const slugName = toSlug(rec.title);
|
|
2118
|
+
const content = [
|
|
2119
|
+
"#!/usr/bin/env bash",
|
|
2120
|
+
`# Auto-generated hook for: ${rec.title}`,
|
|
2121
|
+
`# Hook event: ${hookEvent}`,
|
|
2122
|
+
`# Source: harness-evolve (${rec.id})`,
|
|
2123
|
+
"#",
|
|
2124
|
+
"# TODO: Review and customize this script before use.",
|
|
2125
|
+
"",
|
|
2126
|
+
"# Read hook input from stdin",
|
|
2127
|
+
"INPUT=$(cat)",
|
|
2128
|
+
"",
|
|
2129
|
+
"# Extract relevant fields",
|
|
2130
|
+
`# Adjust jq path based on your ${hookEvent} event schema`,
|
|
2131
|
+
"",
|
|
2132
|
+
`# ${rec.suggested_action}`,
|
|
2133
|
+
"",
|
|
2134
|
+
"# Exit 0 to allow, exit 2 to block",
|
|
2135
|
+
"exit 0"
|
|
2136
|
+
].join("\n");
|
|
2137
|
+
return {
|
|
2138
|
+
type: "hook",
|
|
2139
|
+
filename: `.claude/hooks/evolve-${slugName}.sh`,
|
|
2140
|
+
content,
|
|
2141
|
+
source_recommendation_id: rec.id,
|
|
2142
|
+
metadata: {
|
|
2143
|
+
generated_at: nowISO(),
|
|
2144
|
+
generator_version: GENERATOR_VERSION,
|
|
2145
|
+
pattern_type: rec.pattern_type
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/cli/utils.ts
|
|
2151
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2152
|
+
import { join as join8 } from "path";
|
|
2153
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2154
|
+
import writeFileAtomic9 from "write-file-atomic";
|
|
2155
|
+
var HARNESS_EVOLVE_MARKER = "harness-evolve";
|
|
2156
|
+
var SETTINGS_PATH = join8(
|
|
2157
|
+
process.env.HOME ?? "",
|
|
2158
|
+
".claude",
|
|
2159
|
+
"settings.json"
|
|
2160
|
+
);
|
|
2161
|
+
async function readSettings(settingsPath) {
|
|
2162
|
+
const filePath = settingsPath ?? SETTINGS_PATH;
|
|
2163
|
+
try {
|
|
2164
|
+
const raw = await readFile9(filePath, "utf-8");
|
|
2165
|
+
return JSON.parse(raw);
|
|
2166
|
+
} catch {
|
|
2167
|
+
return {};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async function writeSettings(settings, settingsPath) {
|
|
2171
|
+
const filePath = settingsPath ?? SETTINGS_PATH;
|
|
2172
|
+
await writeFileAtomic9(filePath, JSON.stringify(settings, null, 2));
|
|
2173
|
+
}
|
|
2174
|
+
function mergeHooks(existing, hookCommands) {
|
|
2175
|
+
const hooks = existing.hooks != null ? { ...existing.hooks } : {};
|
|
2176
|
+
for (const hc of hookCommands) {
|
|
2177
|
+
const eventArray = Array.isArray(hooks[hc.event]) ? [...hooks[hc.event]] : [];
|
|
2178
|
+
const alreadyRegistered = eventArray.some((entry) => {
|
|
2179
|
+
const innerHooks = entry.hooks;
|
|
2180
|
+
if (!Array.isArray(innerHooks)) return false;
|
|
2181
|
+
return innerHooks.some(
|
|
2182
|
+
(h) => String(h.command ?? "").includes(HARNESS_EVOLVE_MARKER)
|
|
2183
|
+
);
|
|
2184
|
+
});
|
|
2185
|
+
if (!alreadyRegistered) {
|
|
2186
|
+
const hookEntry = {
|
|
2187
|
+
type: "command",
|
|
2188
|
+
command: hc.command,
|
|
2189
|
+
timeout: hc.timeout
|
|
2190
|
+
};
|
|
2191
|
+
if (hc.async) {
|
|
2192
|
+
hookEntry.async = true;
|
|
2193
|
+
}
|
|
2194
|
+
eventArray.push({
|
|
2195
|
+
matcher: "*",
|
|
2196
|
+
hooks: [hookEntry]
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
hooks[hc.event] = eventArray;
|
|
2200
|
+
}
|
|
2201
|
+
return { ...existing, hooks };
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/delivery/appliers/hook-applier.ts
|
|
2205
|
+
var HookApplier = class {
|
|
2206
|
+
target = "HOOK";
|
|
2207
|
+
canApply(rec) {
|
|
2208
|
+
return rec.confidence === "HIGH" && rec.target === "HOOK";
|
|
2209
|
+
}
|
|
2210
|
+
async apply(rec, options) {
|
|
2211
|
+
try {
|
|
2212
|
+
const artifact = generateHook(rec);
|
|
2213
|
+
if (!artifact) {
|
|
2214
|
+
return {
|
|
2215
|
+
recommendation_id: rec.id,
|
|
2216
|
+
success: false,
|
|
2217
|
+
details: "Generator returned null \u2014 recommendation not applicable for hook generation"
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
const hooksDir = options?.hooksDir ?? join9(process.env.HOME ?? "", ".claude", "hooks");
|
|
2221
|
+
const scriptFilename = basename(artifact.filename);
|
|
2222
|
+
const scriptPath = join9(hooksDir, scriptFilename);
|
|
2223
|
+
try {
|
|
2224
|
+
await access3(scriptPath);
|
|
2225
|
+
return {
|
|
2226
|
+
recommendation_id: rec.id,
|
|
2227
|
+
success: false,
|
|
2228
|
+
details: `Hook file already exists: ${scriptFilename}`
|
|
2229
|
+
};
|
|
2230
|
+
} catch {
|
|
2231
|
+
}
|
|
2232
|
+
await mkdir5(hooksDir, { recursive: true });
|
|
2233
|
+
await writeFile3(scriptPath, artifact.content, "utf-8");
|
|
2234
|
+
await chmod(scriptPath, 493);
|
|
2235
|
+
const settingsPath = options?.settingsPath ?? join9(process.env.HOME ?? "", ".claude", "settings.json");
|
|
2236
|
+
const settings = await readSettings(settingsPath);
|
|
2237
|
+
const backupDir = join9(paths.analysis, "backups");
|
|
2238
|
+
await mkdir5(backupDir, { recursive: true });
|
|
2239
|
+
const backupFile = join9(backupDir, `settings-backup-${rec.id}.json`);
|
|
2240
|
+
try {
|
|
2241
|
+
await copyFile2(settingsPath, backupFile);
|
|
2242
|
+
} catch {
|
|
2243
|
+
await writeFile3(backupFile, JSON.stringify(settings, null, 2), "utf-8");
|
|
2244
|
+
}
|
|
2245
|
+
const eventMatch = artifact.content.match(/# Hook event: (\w+)/);
|
|
2246
|
+
const hookEvent = eventMatch?.[1] ?? "PreToolUse";
|
|
2247
|
+
const merged = mergeHooks(settings, [
|
|
2248
|
+
{
|
|
2249
|
+
event: hookEvent,
|
|
2250
|
+
command: `bash "${scriptPath}"`,
|
|
2251
|
+
timeout: 10,
|
|
2252
|
+
async: true
|
|
2253
|
+
}
|
|
2254
|
+
]);
|
|
2255
|
+
await writeSettings(merged, settingsPath);
|
|
2256
|
+
return {
|
|
2257
|
+
recommendation_id: rec.id,
|
|
2258
|
+
success: true,
|
|
2259
|
+
details: `Created hook script: ${scriptFilename} and registered under ${hookEvent}`
|
|
2260
|
+
};
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2263
|
+
return {
|
|
2264
|
+
recommendation_id: rec.id,
|
|
2265
|
+
success: false,
|
|
2266
|
+
details: message
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// src/delivery/appliers/claude-md-applier.ts
|
|
2273
|
+
import { readFile as readFile10, mkdir as mkdir6 } from "fs/promises";
|
|
2274
|
+
import { join as join10, dirname as dirname4 } from "path";
|
|
2275
|
+
import writeFileAtomic10 from "write-file-atomic";
|
|
2276
|
+
var DESTRUCTIVE_PATTERNS = /* @__PURE__ */ new Set([
|
|
2277
|
+
"scan_stale_reference",
|
|
2278
|
+
"scan_redundancy"
|
|
2279
|
+
]);
|
|
2280
|
+
var ClaudeMdApplier = class {
|
|
2281
|
+
target = "CLAUDE_MD";
|
|
2282
|
+
canApply(rec) {
|
|
2283
|
+
return rec.confidence === "HIGH" && rec.target === "CLAUDE_MD";
|
|
2284
|
+
}
|
|
2285
|
+
async apply(rec, options) {
|
|
2286
|
+
try {
|
|
2287
|
+
if (DESTRUCTIVE_PATTERNS.has(rec.pattern_type)) {
|
|
2288
|
+
return {
|
|
2289
|
+
recommendation_id: rec.id,
|
|
2290
|
+
success: false,
|
|
2291
|
+
details: `Pattern type '${rec.pattern_type}' requires manual review \u2014 cannot safely auto-apply`
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
const claudeMdPath = options?.claudeMdPath ?? join10(process.cwd(), "CLAUDE.md");
|
|
2295
|
+
let existingContent = "";
|
|
2296
|
+
try {
|
|
2297
|
+
existingContent = await readFile10(claudeMdPath, "utf-8");
|
|
2298
|
+
} catch {
|
|
2299
|
+
}
|
|
2300
|
+
if (existingContent) {
|
|
2301
|
+
const backupDir = join10(paths.analysis, "backups");
|
|
2302
|
+
await mkdir6(backupDir, { recursive: true });
|
|
2303
|
+
const backupFile = join10(backupDir, `claudemd-backup-${rec.id}.md`);
|
|
2304
|
+
await writeFileAtomic10(backupFile, existingContent);
|
|
2305
|
+
}
|
|
2306
|
+
const newSection = [
|
|
2307
|
+
"",
|
|
2308
|
+
"",
|
|
2309
|
+
`## ${rec.title}`,
|
|
2310
|
+
"",
|
|
2311
|
+
rec.suggested_action,
|
|
2312
|
+
"",
|
|
2313
|
+
"---",
|
|
2314
|
+
`*Auto-generated by harness-evolve (${rec.id})*`,
|
|
2315
|
+
""
|
|
2316
|
+
].join("\n");
|
|
2317
|
+
const updatedContent = existingContent + newSection;
|
|
2318
|
+
await mkdir6(dirname4(claudeMdPath), { recursive: true });
|
|
2319
|
+
await writeFileAtomic10(claudeMdPath, updatedContent);
|
|
2320
|
+
return {
|
|
2321
|
+
recommendation_id: rec.id,
|
|
2322
|
+
success: true,
|
|
2323
|
+
details: `Appended section: ${rec.title}`
|
|
2324
|
+
};
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2327
|
+
return {
|
|
2328
|
+
recommendation_id: rec.id,
|
|
2329
|
+
success: false,
|
|
2330
|
+
details: message
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// src/delivery/auto-apply.ts
|
|
2337
|
+
registerApplier(new SettingsApplier());
|
|
2338
|
+
registerApplier(new RuleApplier());
|
|
2339
|
+
registerApplier(new HookApplier());
|
|
2340
|
+
registerApplier(new ClaudeMdApplier());
|
|
2341
|
+
async function autoApplyRecommendations(recommendations, options) {
|
|
2342
|
+
const config = await loadConfig();
|
|
2343
|
+
if (!config.delivery.fullAuto) return [];
|
|
2344
|
+
await ensureInit();
|
|
2345
|
+
const stateMap = await getStatusMap();
|
|
2346
|
+
const results = [];
|
|
2347
|
+
const candidates = recommendations.filter(
|
|
2348
|
+
(rec) => rec.confidence === "HIGH" && hasApplier(rec.target) && (stateMap.get(rec.id) ?? "pending") === "pending"
|
|
2349
|
+
);
|
|
2350
|
+
for (const rec of candidates) {
|
|
2351
|
+
const applier = getApplier(rec.target);
|
|
2352
|
+
let result;
|
|
2353
|
+
if (!applier || !applier.canApply(rec)) {
|
|
2354
|
+
result = {
|
|
2355
|
+
recommendation_id: rec.id,
|
|
2356
|
+
success: false,
|
|
2357
|
+
details: `No applicable applier for target '${rec.target}' with pattern_type '${rec.pattern_type}'`
|
|
2358
|
+
};
|
|
2359
|
+
} else {
|
|
2360
|
+
result = await applier.apply(rec, options);
|
|
2361
|
+
}
|
|
2362
|
+
results.push(result);
|
|
2363
|
+
const logEntry = {
|
|
2364
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2365
|
+
recommendation_id: rec.id,
|
|
2366
|
+
target: rec.target,
|
|
2367
|
+
action: rec.suggested_action,
|
|
2368
|
+
success: result.success,
|
|
2369
|
+
details: result.details,
|
|
2370
|
+
backup_path: void 0
|
|
2371
|
+
};
|
|
2372
|
+
await appendFile3(
|
|
2373
|
+
paths.autoApplyLog,
|
|
2374
|
+
JSON.stringify(logEntry) + "\n",
|
|
2375
|
+
"utf-8"
|
|
2376
|
+
);
|
|
2377
|
+
if (result.success) {
|
|
2378
|
+
await updateStatus(rec.id, "applied", `Auto-applied: ${result.details}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return results;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// src/scan/context-builder.ts
|
|
2385
|
+
import { readFile as readFile11, readdir as readdir3 } from "fs/promises";
|
|
2386
|
+
import { join as join11, basename as basename2 } from "path";
|
|
2387
|
+
|
|
2388
|
+
// src/scan/schemas.ts
|
|
2389
|
+
import { z as z10 } from "zod/v4";
|
|
2390
|
+
var scanContextSchema = z10.object({
|
|
2391
|
+
generated_at: z10.iso.datetime(),
|
|
2392
|
+
project_root: z10.string(),
|
|
2393
|
+
claude_md_files: z10.array(
|
|
2394
|
+
z10.object({
|
|
2395
|
+
path: z10.string(),
|
|
2396
|
+
scope: z10.enum(["user", "project", "local"]),
|
|
2397
|
+
content: z10.string(),
|
|
2398
|
+
line_count: z10.number(),
|
|
2399
|
+
headings: z10.array(z10.string()),
|
|
2400
|
+
references: z10.array(z10.string())
|
|
2401
|
+
})
|
|
2402
|
+
),
|
|
2403
|
+
rules: z10.array(
|
|
2404
|
+
z10.object({
|
|
2405
|
+
path: z10.string(),
|
|
2406
|
+
filename: z10.string(),
|
|
2407
|
+
content: z10.string(),
|
|
2408
|
+
frontmatter: z10.object({
|
|
2409
|
+
paths: z10.array(z10.string()).optional()
|
|
2410
|
+
}).optional(),
|
|
2411
|
+
headings: z10.array(z10.string())
|
|
2412
|
+
})
|
|
2413
|
+
),
|
|
2414
|
+
settings: z10.object({
|
|
2415
|
+
user: z10.unknown().nullable(),
|
|
2416
|
+
project: z10.unknown().nullable(),
|
|
2417
|
+
local: z10.unknown().nullable()
|
|
2418
|
+
}),
|
|
2419
|
+
commands: z10.array(
|
|
2420
|
+
z10.object({
|
|
2421
|
+
path: z10.string(),
|
|
2422
|
+
name: z10.string(),
|
|
2423
|
+
content: z10.string()
|
|
2424
|
+
})
|
|
2425
|
+
),
|
|
2426
|
+
hooks_registered: z10.array(
|
|
2427
|
+
z10.object({
|
|
2428
|
+
event: z10.string(),
|
|
2429
|
+
scope: z10.enum(["user", "project", "local"]),
|
|
2430
|
+
type: z10.string(),
|
|
2431
|
+
command: z10.string().optional()
|
|
2432
|
+
})
|
|
2433
|
+
)
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
// src/scan/context-builder.ts
|
|
2437
|
+
async function readFileSafe(path) {
|
|
2438
|
+
try {
|
|
2439
|
+
return await readFile11(path, "utf-8");
|
|
2440
|
+
} catch {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function extractHeadings(content) {
|
|
2445
|
+
const headings = [];
|
|
2446
|
+
const regex = /^#{1,6}\s+(.+)$/gm;
|
|
2447
|
+
let match;
|
|
2448
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2449
|
+
headings.push(match[1].trim());
|
|
2450
|
+
}
|
|
2451
|
+
return headings;
|
|
2452
|
+
}
|
|
2453
|
+
function extractReferences(content) {
|
|
2454
|
+
const refs = [];
|
|
2455
|
+
const regex = /@([\w./-]+)/g;
|
|
2456
|
+
let match;
|
|
2457
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2458
|
+
const ref = match[1];
|
|
2459
|
+
const idx = match.index;
|
|
2460
|
+
if (idx > 0 && /\w/.test(content[idx - 1])) {
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
const cleaned = ref.replace(/\.$/, "");
|
|
2464
|
+
refs.push(cleaned);
|
|
2465
|
+
}
|
|
2466
|
+
return refs;
|
|
2467
|
+
}
|
|
2468
|
+
async function readClaudeMdFiles(cwd, home) {
|
|
2469
|
+
const locations = [
|
|
2470
|
+
{ path: join11(cwd, "CLAUDE.md"), scope: "project" },
|
|
2471
|
+
{ path: join11(cwd, ".claude", "CLAUDE.md"), scope: "local" },
|
|
2472
|
+
{ path: join11(home, ".claude", "CLAUDE.md"), scope: "user" }
|
|
2473
|
+
];
|
|
2474
|
+
const files = [];
|
|
2475
|
+
for (const loc of locations) {
|
|
2476
|
+
const content = await readFileSafe(loc.path);
|
|
2477
|
+
if (content !== null) {
|
|
2478
|
+
files.push({
|
|
2479
|
+
path: loc.path,
|
|
2480
|
+
scope: loc.scope,
|
|
2481
|
+
content,
|
|
2482
|
+
line_count: content.split("\n").length,
|
|
2483
|
+
headings: extractHeadings(content),
|
|
2484
|
+
references: extractReferences(content)
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return files;
|
|
2489
|
+
}
|
|
2490
|
+
async function collectMdFiles(dir) {
|
|
2491
|
+
const results = [];
|
|
2492
|
+
try {
|
|
2493
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2494
|
+
for (const entry of entries) {
|
|
2495
|
+
const fullPath = join11(dir, entry.name);
|
|
2496
|
+
if (entry.isDirectory()) {
|
|
2497
|
+
const nested = await collectMdFiles(fullPath);
|
|
2498
|
+
results.push(...nested);
|
|
2499
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2500
|
+
results.push(fullPath);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
return results;
|
|
2506
|
+
}
|
|
2507
|
+
function parseFrontmatter(content) {
|
|
2508
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2509
|
+
if (!match) return void 0;
|
|
2510
|
+
const frontmatter = match[1];
|
|
2511
|
+
const pathsMatch = frontmatter.match(
|
|
2512
|
+
/paths:\s*\n((?:\s*-\s*.+\n?)*)/
|
|
2513
|
+
);
|
|
2514
|
+
if (!pathsMatch) return {};
|
|
2515
|
+
const paths2 = pathsMatch[1].split("\n").map((line) => line.replace(/^\s*-\s*/, "").trim()).filter((line) => line.length > 0);
|
|
2516
|
+
return paths2.length > 0 ? { paths: paths2 } : {};
|
|
2517
|
+
}
|
|
2518
|
+
async function readRuleFiles(cwd) {
|
|
2519
|
+
const rulesDir = join11(cwd, ".claude", "rules");
|
|
2520
|
+
const mdFiles = await collectMdFiles(rulesDir);
|
|
2521
|
+
const rules = [];
|
|
2522
|
+
for (const filePath of mdFiles) {
|
|
2523
|
+
const content = await readFileSafe(filePath);
|
|
2524
|
+
if (content !== null) {
|
|
2525
|
+
rules.push({
|
|
2526
|
+
path: filePath,
|
|
2527
|
+
filename: basename2(filePath),
|
|
2528
|
+
content,
|
|
2529
|
+
frontmatter: parseFrontmatter(content),
|
|
2530
|
+
headings: extractHeadings(content)
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return rules;
|
|
2535
|
+
}
|
|
2536
|
+
async function readAllSettings(cwd, home) {
|
|
2537
|
+
const settingsPaths = {
|
|
2538
|
+
user: join11(home, ".claude", "settings.json"),
|
|
2539
|
+
project: join11(cwd, ".claude", "settings.json"),
|
|
2540
|
+
local: join11(cwd, ".claude", "settings.local.json")
|
|
2541
|
+
};
|
|
2542
|
+
const readJsonSafe = async (path) => {
|
|
2543
|
+
try {
|
|
2544
|
+
const raw = await readFile11(path, "utf-8");
|
|
2545
|
+
return JSON.parse(raw);
|
|
2546
|
+
} catch {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
const [user, project, local] = await Promise.all([
|
|
2551
|
+
readJsonSafe(settingsPaths.user),
|
|
2552
|
+
readJsonSafe(settingsPaths.project),
|
|
2553
|
+
readJsonSafe(settingsPaths.local)
|
|
2554
|
+
]);
|
|
2555
|
+
return { user, project, local };
|
|
2556
|
+
}
|
|
2557
|
+
async function readCommandFiles(cwd) {
|
|
2558
|
+
const commandsDir = join11(cwd, ".claude", "commands");
|
|
2559
|
+
const commands = [];
|
|
2560
|
+
try {
|
|
2561
|
+
const entries = await readdir3(commandsDir, { withFileTypes: true });
|
|
2562
|
+
for (const entry of entries) {
|
|
2563
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2564
|
+
const filePath = join11(commandsDir, entry.name);
|
|
2565
|
+
const content = await readFileSafe(filePath);
|
|
2566
|
+
if (content !== null) {
|
|
2567
|
+
commands.push({
|
|
2568
|
+
path: filePath,
|
|
2569
|
+
name: entry.name.replace(/\.md$/, ""),
|
|
2570
|
+
content
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
} catch {
|
|
2576
|
+
}
|
|
2577
|
+
return commands;
|
|
2578
|
+
}
|
|
2579
|
+
function extractHooksFromAllSettings(settings) {
|
|
2580
|
+
const hooks = [];
|
|
2581
|
+
const extractFromScope = (settingsObj, scope) => {
|
|
2582
|
+
if (!settingsObj || typeof settingsObj !== "object") return;
|
|
2583
|
+
const obj = settingsObj;
|
|
2584
|
+
if (!obj.hooks || typeof obj.hooks !== "object") return;
|
|
2585
|
+
const hooksConfig = obj.hooks;
|
|
2586
|
+
for (const [event, defs] of Object.entries(hooksConfig)) {
|
|
2587
|
+
if (!Array.isArray(defs)) continue;
|
|
2588
|
+
for (const def of defs) {
|
|
2589
|
+
if (!def || typeof def !== "object") continue;
|
|
2590
|
+
const hookDef = def;
|
|
2591
|
+
const type = String(hookDef.type ?? "command");
|
|
2592
|
+
const command = typeof hookDef.command === "string" ? hookDef.command : void 0;
|
|
2593
|
+
hooks.push({ event, scope, type, command });
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
extractFromScope(settings.user, "user");
|
|
2598
|
+
extractFromScope(settings.project, "project");
|
|
2599
|
+
extractFromScope(settings.local, "local");
|
|
2600
|
+
return hooks;
|
|
2601
|
+
}
|
|
2602
|
+
async function buildScanContext(cwd, home) {
|
|
2603
|
+
const homeDir = home ?? process.env.HOME ?? "";
|
|
2604
|
+
const [claudeMdFiles, rules, settings, commands] = await Promise.all([
|
|
2605
|
+
readClaudeMdFiles(cwd, homeDir),
|
|
2606
|
+
readRuleFiles(cwd),
|
|
2607
|
+
readAllSettings(cwd, homeDir),
|
|
2608
|
+
readCommandFiles(cwd)
|
|
2609
|
+
]);
|
|
2610
|
+
const hooksRegistered = extractHooksFromAllSettings(settings);
|
|
2611
|
+
const ctx = {
|
|
2612
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2613
|
+
project_root: cwd,
|
|
2614
|
+
claude_md_files: claudeMdFiles,
|
|
2615
|
+
rules,
|
|
2616
|
+
settings,
|
|
2617
|
+
commands,
|
|
2618
|
+
hooks_registered: hooksRegistered
|
|
2619
|
+
};
|
|
2620
|
+
return scanContextSchema.parse(ctx);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// src/scan/scanners/redundancy.ts
|
|
2624
|
+
function normalizeText(text) {
|
|
2625
|
+
return text.toLowerCase().trim().replace(/\s+/g, " ");
|
|
2626
|
+
}
|
|
2627
|
+
function scanRedundancy(context) {
|
|
2628
|
+
const recommendations = [];
|
|
2629
|
+
let index = 0;
|
|
2630
|
+
const claudeMdHeadings = context.claude_md_files.flatMap(
|
|
2631
|
+
(f) => f.headings.map((h) => ({ heading: normalizeText(h), source: f.path }))
|
|
2632
|
+
);
|
|
2633
|
+
const ruleHeadings = context.rules.flatMap(
|
|
2634
|
+
(r) => r.headings.map((h) => ({ heading: normalizeText(h), source: r.path }))
|
|
2635
|
+
);
|
|
2636
|
+
for (const cmdH of claudeMdHeadings) {
|
|
2637
|
+
const match = ruleHeadings.find((rH) => rH.heading === cmdH.heading);
|
|
2638
|
+
if (match) {
|
|
2639
|
+
recommendations.push({
|
|
2640
|
+
id: `rec-scan-redundancy-${index++}`,
|
|
2641
|
+
target: "RULE",
|
|
2642
|
+
confidence: "MEDIUM",
|
|
2643
|
+
pattern_type: "scan_redundancy",
|
|
2644
|
+
title: `Redundant section: "${cmdH.heading}"`,
|
|
2645
|
+
description: `The heading "${cmdH.heading}" appears in both ${cmdH.source} and ${match.source}. This may indicate duplicated instructions.`,
|
|
2646
|
+
evidence: {
|
|
2647
|
+
count: 2,
|
|
2648
|
+
examples: [cmdH.source, match.source]
|
|
2649
|
+
},
|
|
2650
|
+
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."
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const rulesByHeadingSet = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const rule of context.rules) {
|
|
2656
|
+
const key = rule.headings.map((h) => normalizeText(h)).sort().join("||");
|
|
2657
|
+
if (!key) continue;
|
|
2658
|
+
const existing = rulesByHeadingSet.get(key) ?? [];
|
|
2659
|
+
existing.push(rule.path);
|
|
2660
|
+
rulesByHeadingSet.set(key, existing);
|
|
2661
|
+
}
|
|
2662
|
+
for (const [, paths2] of rulesByHeadingSet) {
|
|
2663
|
+
if (paths2.length < 2) continue;
|
|
2664
|
+
recommendations.push({
|
|
2665
|
+
id: `rec-scan-redundancy-${index++}`,
|
|
2666
|
+
target: "RULE",
|
|
2667
|
+
confidence: "MEDIUM",
|
|
2668
|
+
pattern_type: "scan_redundancy",
|
|
2669
|
+
title: `Duplicate rule files detected (${paths2.length} files with same headings)`,
|
|
2670
|
+
description: `${paths2.length} rule files share the same heading structure: ${paths2.join(", ")}. They may contain redundant content.`,
|
|
2671
|
+
evidence: {
|
|
2672
|
+
count: paths2.length,
|
|
2673
|
+
examples: paths2.slice(0, 3)
|
|
2674
|
+
},
|
|
2675
|
+
suggested_action: "Review these rule files and merge them into a single file, or differentiate their content."
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
return recommendations;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/scan/scanners/mechanization.ts
|
|
2682
|
+
var MECHANIZATION_INDICATORS = [
|
|
2683
|
+
{ regex: /always\s+run\s+["`']?(\S+)/i, hookEvent: "PreToolUse", label: "always run" },
|
|
2684
|
+
{
|
|
2685
|
+
regex: /before\s+committing?,?\s+run\s+["`']?(\S+)/i,
|
|
2686
|
+
hookEvent: "PreToolUse",
|
|
2687
|
+
label: "pre-commit check"
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
regex: /after\s+every\s+(?:edit|change|write)/i,
|
|
2691
|
+
hookEvent: "PostToolUse",
|
|
2692
|
+
label: "post-edit action"
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
regex: /must\s+(?:always\s+)?check\s+["`']?(\S+)/i,
|
|
2696
|
+
hookEvent: "PreToolUse",
|
|
2697
|
+
label: "mandatory check"
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
regex: /never\s+(?:allow|permit|run)\s+["`']?(\S+)/i,
|
|
2701
|
+
hookEvent: "PreToolUse",
|
|
2702
|
+
label: "forbidden operation"
|
|
2703
|
+
},
|
|
2704
|
+
{
|
|
2705
|
+
regex: /forbidden.*(?:rm\s+-rf|drop\s+|delete\s+|truncate)/i,
|
|
2706
|
+
hookEvent: "PreToolUse",
|
|
2707
|
+
label: "dangerous command guard"
|
|
2708
|
+
}
|
|
2709
|
+
];
|
|
2710
|
+
function scanMechanization(context) {
|
|
2711
|
+
const recommendations = [];
|
|
2712
|
+
let index = 0;
|
|
2713
|
+
const allTextSources = [
|
|
2714
|
+
...context.claude_md_files.map((f) => ({ content: f.content, source: f.path })),
|
|
2715
|
+
...context.rules.map((r) => ({ content: r.content, source: r.path }))
|
|
2716
|
+
];
|
|
2717
|
+
for (const source of allTextSources) {
|
|
2718
|
+
for (const indicator of MECHANIZATION_INDICATORS) {
|
|
2719
|
+
const match = source.content.match(indicator.regex);
|
|
2720
|
+
if (!match) continue;
|
|
2721
|
+
const alreadyCovered = context.hooks_registered.some(
|
|
2722
|
+
(h) => h.event === indicator.hookEvent
|
|
2723
|
+
);
|
|
2724
|
+
if (alreadyCovered) continue;
|
|
2725
|
+
recommendations.push({
|
|
2726
|
+
id: `rec-scan-mechanize-${index++}`,
|
|
2727
|
+
target: "HOOK",
|
|
2728
|
+
confidence: "MEDIUM",
|
|
2729
|
+
pattern_type: "scan_missing_mechanization",
|
|
2730
|
+
title: `Mechanizable rule: "${match[0].substring(0, 60)}"`,
|
|
2731
|
+
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.`,
|
|
2732
|
+
evidence: {
|
|
2733
|
+
count: 1,
|
|
2734
|
+
examples: [match[0].substring(0, 100)]
|
|
2735
|
+
},
|
|
2736
|
+
suggested_action: `Create a ${indicator.hookEvent} hook to enforce this rule automatically. See Claude Code hooks docs for ${indicator.hookEvent} event.`
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return recommendations;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// src/scan/scanners/staleness.ts
|
|
2744
|
+
import { access as access4 } from "fs/promises";
|
|
2745
|
+
import { constants as constants2 } from "fs";
|
|
2746
|
+
import { resolve, dirname as dirname5 } from "path";
|
|
2747
|
+
async function fileExistsOnDisk(filePath) {
|
|
2748
|
+
try {
|
|
2749
|
+
await access4(filePath, constants2.F_OK);
|
|
2750
|
+
return true;
|
|
2751
|
+
} catch {
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
function extractPathFromCommand(command) {
|
|
2756
|
+
const quotedMatch = command.match(/(?:node|sh|bash|python)\s+["']([^"']+)["']/i);
|
|
2757
|
+
if (quotedMatch) return quotedMatch[1];
|
|
2758
|
+
const unquotedMatch = command.match(
|
|
2759
|
+
/(?:node|sh|bash|python)\s+(\S+\.(?:js|ts|sh|py|mjs|cjs))/i
|
|
2760
|
+
);
|
|
2761
|
+
if (unquotedMatch) return unquotedMatch[1];
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
async function scanStaleness(context) {
|
|
2765
|
+
const recommendations = [];
|
|
2766
|
+
let index = 0;
|
|
2767
|
+
for (const claudeMd of context.claude_md_files) {
|
|
2768
|
+
for (const ref of claudeMd.references) {
|
|
2769
|
+
const resolved = resolve(dirname5(claudeMd.path), ref);
|
|
2770
|
+
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);
|
|
2771
|
+
if (inContext) continue;
|
|
2772
|
+
const existsOnDisk = await fileExistsOnDisk(resolved);
|
|
2773
|
+
if (existsOnDisk) continue;
|
|
2774
|
+
recommendations.push({
|
|
2775
|
+
id: `rec-scan-stale-${index++}`,
|
|
2776
|
+
target: "CLAUDE_MD",
|
|
2777
|
+
confidence: "HIGH",
|
|
2778
|
+
pattern_type: "scan_stale_reference",
|
|
2779
|
+
title: `Stale reference: @${ref}`,
|
|
2780
|
+
description: `${claudeMd.path} references @${ref}, but this file does not exist.`,
|
|
2781
|
+
evidence: {
|
|
2782
|
+
count: 1,
|
|
2783
|
+
examples: [`@${ref} in ${claudeMd.path}`]
|
|
2784
|
+
},
|
|
2785
|
+
suggested_action: `Remove the @${ref} reference from ${claudeMd.path}, or create the missing file.`
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
for (const hook of context.hooks_registered) {
|
|
2790
|
+
if (!hook.command) continue;
|
|
2791
|
+
const scriptPath = extractPathFromCommand(hook.command);
|
|
2792
|
+
if (!scriptPath) continue;
|
|
2793
|
+
const exists = await fileExistsOnDisk(scriptPath);
|
|
2794
|
+
if (exists) continue;
|
|
2795
|
+
recommendations.push({
|
|
2796
|
+
id: `rec-scan-stale-${index++}`,
|
|
2797
|
+
target: "SETTINGS",
|
|
2798
|
+
confidence: "HIGH",
|
|
2799
|
+
pattern_type: "scan_stale_reference",
|
|
2800
|
+
title: `Stale hook script: ${scriptPath}`,
|
|
2801
|
+
description: `Hook (${hook.event}, ${hook.scope}) references script "${scriptPath}", but this file does not exist. The hook will fail when triggered.`,
|
|
2802
|
+
evidence: {
|
|
2803
|
+
count: 1,
|
|
2804
|
+
examples: [`${hook.event} hook command: ${hook.command}`]
|
|
2805
|
+
},
|
|
2806
|
+
suggested_action: `Create the missing script at "${scriptPath}", or update the hook command in settings.json.`
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
return recommendations;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// src/scan/scanners/index.ts
|
|
2813
|
+
var scanners = [scanRedundancy, scanMechanization, scanStaleness];
|
|
2814
|
+
|
|
2815
|
+
// src/scan/index.ts
|
|
2816
|
+
async function runDeepScan(cwd, home) {
|
|
2817
|
+
const scanContext = await buildScanContext(cwd, home);
|
|
2818
|
+
const recommendations = [];
|
|
2819
|
+
for (const scanner of scanners) {
|
|
2820
|
+
try {
|
|
2821
|
+
const result = await scanner(scanContext);
|
|
2822
|
+
recommendations.push(...result);
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
console.error(
|
|
2825
|
+
`Scanner error: ${err instanceof Error ? err.message : String(err)}`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return {
|
|
2830
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2831
|
+
scan_context: scanContext,
|
|
2832
|
+
recommendations
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/generators/skill-generator.ts
|
|
2837
|
+
function generateSkill(rec) {
|
|
2838
|
+
if (rec.target !== "SKILL") return null;
|
|
2839
|
+
if (rec.pattern_type !== "long_prompt") return null;
|
|
2840
|
+
const promptPreview = rec.evidence.examples[0] ?? "";
|
|
2841
|
+
const slugName = toSlug(rec.title);
|
|
2842
|
+
const firstSentence = rec.description.split(".")[0];
|
|
2843
|
+
const content = [
|
|
2844
|
+
"---",
|
|
2845
|
+
`name: ${escapeYaml(slugName)}`,
|
|
2846
|
+
`description: ${escapeYaml(firstSentence)}`,
|
|
2847
|
+
"---",
|
|
2848
|
+
"",
|
|
2849
|
+
`# ${rec.title}`,
|
|
2850
|
+
"",
|
|
2851
|
+
"## Instructions",
|
|
2852
|
+
"",
|
|
2853
|
+
promptPreview,
|
|
2854
|
+
"",
|
|
2855
|
+
"---",
|
|
2856
|
+
`*Auto-generated by harness-evolve (${rec.id})*`
|
|
2857
|
+
].join("\n");
|
|
2858
|
+
return {
|
|
2859
|
+
type: "skill",
|
|
2860
|
+
filename: `.claude/commands/${slugName}.md`,
|
|
2861
|
+
content,
|
|
2862
|
+
source_recommendation_id: rec.id,
|
|
2863
|
+
metadata: {
|
|
2864
|
+
generated_at: nowISO(),
|
|
2865
|
+
generator_version: GENERATOR_VERSION,
|
|
2866
|
+
pattern_type: rec.pattern_type
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/generators/claude-md-generator.ts
|
|
2872
|
+
function buildStaleReferencePatch(rec) {
|
|
2873
|
+
const staleRef = rec.evidence.examples[0] ?? "";
|
|
2874
|
+
return [
|
|
2875
|
+
"--- a/CLAUDE.md",
|
|
2876
|
+
"+++ b/CLAUDE.md",
|
|
2877
|
+
"@@ Stale reference removal @@",
|
|
2878
|
+
`- ${staleRef}`,
|
|
2879
|
+
`+ # (removed stale reference: ${staleRef})`
|
|
2880
|
+
].join("\n");
|
|
2881
|
+
}
|
|
2882
|
+
function buildRedundancyPatch(rec) {
|
|
2883
|
+
return [
|
|
2884
|
+
"--- a/CLAUDE.md",
|
|
2885
|
+
"+++ b/CLAUDE.md",
|
|
2886
|
+
`@@ Redundancy consolidation: ${rec.title} @@`,
|
|
2887
|
+
"+ # Consolidation needed",
|
|
2888
|
+
"+",
|
|
2889
|
+
`+ ${rec.suggested_action}`
|
|
2890
|
+
].join("\n");
|
|
2891
|
+
}
|
|
2892
|
+
function buildGenericPatch(rec) {
|
|
2893
|
+
return [
|
|
2894
|
+
"--- a/CLAUDE.md",
|
|
2895
|
+
"+++ b/CLAUDE.md",
|
|
2896
|
+
`@@ ${rec.title} @@`,
|
|
2897
|
+
`+ ## ${rec.title}`,
|
|
2898
|
+
"+",
|
|
2899
|
+
`+ ${rec.suggested_action}`
|
|
2900
|
+
].join("\n");
|
|
2901
|
+
}
|
|
2902
|
+
function generateClaudeMdPatch(rec) {
|
|
2903
|
+
if (rec.target !== "CLAUDE_MD") return null;
|
|
2904
|
+
let patchContent;
|
|
2905
|
+
switch (rec.pattern_type) {
|
|
2906
|
+
case "scan_stale_reference":
|
|
2907
|
+
patchContent = buildStaleReferencePatch(rec);
|
|
2908
|
+
break;
|
|
2909
|
+
case "scan_redundancy":
|
|
2910
|
+
patchContent = buildRedundancyPatch(rec);
|
|
2911
|
+
break;
|
|
2912
|
+
default:
|
|
2913
|
+
patchContent = buildGenericPatch(rec);
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
return {
|
|
2917
|
+
type: "claude_md_patch",
|
|
2918
|
+
filename: "CLAUDE.md",
|
|
2919
|
+
content: patchContent,
|
|
2920
|
+
source_recommendation_id: rec.id,
|
|
2921
|
+
metadata: {
|
|
2922
|
+
generated_at: nowISO(),
|
|
2923
|
+
generator_version: GENERATOR_VERSION,
|
|
2924
|
+
pattern_type: rec.pattern_type
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// src/cli/scan.ts
|
|
2930
|
+
var CONFIDENCE_ORDER2 = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
2931
|
+
function registerScanCommand(program) {
|
|
2932
|
+
program.command("scan").description("Run deep configuration scan and output results as JSON").action(async () => {
|
|
2933
|
+
try {
|
|
2934
|
+
const result = await runDeepScan(process.cwd());
|
|
2935
|
+
const sorted = [...result.recommendations].sort(
|
|
2936
|
+
(a, b) => (CONFIDENCE_ORDER2[a.confidence] ?? 3) - (CONFIDENCE_ORDER2[b.confidence] ?? 3)
|
|
2937
|
+
);
|
|
2938
|
+
const output = {
|
|
2939
|
+
generated_at: result.generated_at,
|
|
2940
|
+
recommendation_count: sorted.length,
|
|
2941
|
+
recommendations: sorted
|
|
2942
|
+
};
|
|
2943
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2944
|
+
} catch (err) {
|
|
2945
|
+
console.log(JSON.stringify({
|
|
2946
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2947
|
+
recommendations: []
|
|
2948
|
+
}, null, 2));
|
|
2949
|
+
process.exitCode = 1;
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// src/cli/apply.ts
|
|
2955
|
+
import { readFile as readFile12, appendFile as appendFile4 } from "fs/promises";
|
|
2956
|
+
var CONFIDENCE_ORDER3 = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
2957
|
+
async function loadAnalysisResult() {
|
|
2958
|
+
try {
|
|
2959
|
+
const raw = await readFile12(paths.analysisResult, "utf-8");
|
|
2960
|
+
const parsed = analysisResultSchema.parse(JSON.parse(raw));
|
|
2961
|
+
return parsed.recommendations;
|
|
2962
|
+
} catch {
|
|
2963
|
+
return [];
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
function registerPendingCommand(program) {
|
|
2967
|
+
program.command("pending").description("List pending recommendations as JSON").action(async () => {
|
|
2968
|
+
const allRecs = await loadAnalysisResult();
|
|
2969
|
+
const state = await loadState();
|
|
2970
|
+
const statusMap = new Map(state.entries.map((e) => [e.id, e.status]));
|
|
2971
|
+
const pending = allRecs.filter((rec) => {
|
|
2972
|
+
const status = statusMap.get(rec.id);
|
|
2973
|
+
return status === void 0 || status === "pending";
|
|
2974
|
+
}).sort(
|
|
2975
|
+
(a, b) => (CONFIDENCE_ORDER3[a.confidence] ?? 3) - (CONFIDENCE_ORDER3[b.confidence] ?? 3)
|
|
2976
|
+
);
|
|
2977
|
+
console.log(JSON.stringify({ pending, count: pending.length }, null, 2));
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
function registerApplyOneCommand(program) {
|
|
2981
|
+
program.command("apply-one").description("Apply a single recommendation by ID").argument("<id>", "Recommendation ID to apply").action(async (id) => {
|
|
2982
|
+
try {
|
|
2983
|
+
const allRecs = await loadAnalysisResult();
|
|
2984
|
+
const rec = allRecs.find((r) => r.id === id);
|
|
2985
|
+
if (!rec) {
|
|
2986
|
+
console.log(JSON.stringify({
|
|
2987
|
+
recommendation_id: id,
|
|
2988
|
+
success: false,
|
|
2989
|
+
details: `Recommendation '${id}' not found`
|
|
2990
|
+
}));
|
|
2991
|
+
process.exitCode = 1;
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const applier = getApplier(rec.target);
|
|
2995
|
+
if (!applier || !applier.canApply(rec)) {
|
|
2996
|
+
console.log(JSON.stringify({
|
|
2997
|
+
recommendation_id: id,
|
|
2998
|
+
success: false,
|
|
2999
|
+
details: `No applicable applier for target '${rec.target}'`
|
|
3000
|
+
}));
|
|
3001
|
+
process.exitCode = 1;
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
const result = await applier.apply(rec);
|
|
3005
|
+
const logEntry = {
|
|
3006
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3007
|
+
recommendation_id: rec.id,
|
|
3008
|
+
target: rec.target,
|
|
3009
|
+
action: rec.suggested_action,
|
|
3010
|
+
success: result.success,
|
|
3011
|
+
details: result.details
|
|
3012
|
+
};
|
|
3013
|
+
try {
|
|
3014
|
+
await appendFile4(paths.autoApplyLog, JSON.stringify(logEntry) + "\n", "utf-8");
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
3017
|
+
if (result.success) {
|
|
3018
|
+
await updateStatus(id, "applied", `Applied via /evolve:apply: ${result.details}`);
|
|
3019
|
+
}
|
|
3020
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
console.log(JSON.stringify({
|
|
3023
|
+
recommendation_id: id,
|
|
3024
|
+
success: false,
|
|
3025
|
+
details: err instanceof Error ? err.message : String(err)
|
|
3026
|
+
}));
|
|
3027
|
+
process.exitCode = 1;
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
function registerDismissCommand(program) {
|
|
3032
|
+
program.command("dismiss").description("Permanently dismiss a recommendation by ID").argument("<id>", "Recommendation ID to dismiss").action(async (id) => {
|
|
3033
|
+
try {
|
|
3034
|
+
await updateStatus(id, "dismissed", "Dismissed by user via /evolve:apply");
|
|
3035
|
+
console.log(JSON.stringify({ id, status: "dismissed" }, null, 2));
|
|
3036
|
+
} catch (err) {
|
|
3037
|
+
console.log(JSON.stringify({
|
|
3038
|
+
id,
|
|
3039
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3040
|
+
}));
|
|
3041
|
+
process.exitCode = 1;
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
export {
|
|
3046
|
+
ClaudeMdApplier,
|
|
3047
|
+
GENERATOR_VERSION,
|
|
3048
|
+
HookApplier,
|
|
3049
|
+
SCRUB_PATTERNS,
|
|
3050
|
+
adjustConfidence,
|
|
3051
|
+
analysisConfigSchema,
|
|
3052
|
+
analysisResultSchema,
|
|
3053
|
+
analyze,
|
|
3054
|
+
appendLogEntry,
|
|
3055
|
+
autoApplyLogEntrySchema,
|
|
3056
|
+
autoApplyRecommendations,
|
|
3057
|
+
buildNotification,
|
|
3058
|
+
buildScanContext,
|
|
3059
|
+
checkAndTriggerAnalysis,
|
|
3060
|
+
classifyOnboarding,
|
|
3061
|
+
clearNotificationFlag,
|
|
3062
|
+
computeExperienceLevel,
|
|
3063
|
+
computeOutcomeSummaries,
|
|
3064
|
+
confidenceSchema,
|
|
3065
|
+
configSchema,
|
|
3066
|
+
counterSchema,
|
|
3067
|
+
ensureInit,
|
|
3068
|
+
environmentSnapshotSchema,
|
|
3069
|
+
experienceLevelSchema,
|
|
3070
|
+
experienceTierSchema,
|
|
3071
|
+
generateClaudeMdPatch,
|
|
3072
|
+
generateHook,
|
|
3073
|
+
generateSkill,
|
|
3074
|
+
generatedArtifactSchema,
|
|
3075
|
+
getStatusMap,
|
|
3076
|
+
handleStop,
|
|
3077
|
+
hasNotificationFlag,
|
|
3078
|
+
hookCommonSchema,
|
|
3079
|
+
incrementCounter,
|
|
3080
|
+
loadConfig,
|
|
3081
|
+
loadOutcomeHistory,
|
|
3082
|
+
loadState,
|
|
3083
|
+
outcomeEntrySchema,
|
|
3084
|
+
outcomeSummarySchema,
|
|
3085
|
+
paths,
|
|
3086
|
+
permissionEntrySchema,
|
|
3087
|
+
permissionRequestInputSchema,
|
|
3088
|
+
postToolUseFailureInputSchema,
|
|
3089
|
+
postToolUseInputSchema,
|
|
3090
|
+
preProcess,
|
|
3091
|
+
preToolUseInputSchema,
|
|
3092
|
+
promptEntrySchema,
|
|
3093
|
+
readCounter,
|
|
3094
|
+
readFromStream,
|
|
3095
|
+
readLogEntries,
|
|
3096
|
+
readNotificationFlagCount,
|
|
3097
|
+
readStdin,
|
|
3098
|
+
recommendationSchema,
|
|
3099
|
+
recommendationStateEntrySchema,
|
|
3100
|
+
recommendationStateSchema,
|
|
3101
|
+
recommendationStatusSchema,
|
|
3102
|
+
registerApplyOneCommand,
|
|
3103
|
+
registerDismissCommand,
|
|
3104
|
+
registerPendingCommand,
|
|
3105
|
+
registerScanCommand,
|
|
3106
|
+
renderRecommendations,
|
|
3107
|
+
resetCounter,
|
|
3108
|
+
rotateRecommendations,
|
|
3109
|
+
routingTargetSchema,
|
|
3110
|
+
runAnalysis,
|
|
3111
|
+
runDeepScan,
|
|
3112
|
+
saveState,
|
|
3113
|
+
scanContextSchema,
|
|
3114
|
+
scanEnvironment,
|
|
3115
|
+
scanMechanization,
|
|
3116
|
+
scanRedundancy,
|
|
3117
|
+
scanStaleness,
|
|
3118
|
+
scrubObject,
|
|
3119
|
+
scrubString,
|
|
3120
|
+
sessionEntrySchema,
|
|
3121
|
+
stopInputSchema,
|
|
3122
|
+
summarizeToolInput,
|
|
3123
|
+
summarySchema,
|
|
3124
|
+
toolEntrySchema,
|
|
3125
|
+
trackOutcomes,
|
|
3126
|
+
updateStatus,
|
|
3127
|
+
userPromptSubmitInputSchema,
|
|
3128
|
+
writeAnalysisResult,
|
|
3129
|
+
writeNotificationFlag
|
|
3130
|
+
};
|
|
3131
|
+
//# sourceMappingURL=index.js.map
|