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
|
@@ -0,0 +1,1609 @@
|
|
|
1
|
+
// src/schemas/hook-input.ts
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
var hookCommonSchema = z.object({
|
|
4
|
+
session_id: z.string(),
|
|
5
|
+
transcript_path: z.string(),
|
|
6
|
+
cwd: z.string(),
|
|
7
|
+
permission_mode: z.string()
|
|
8
|
+
});
|
|
9
|
+
var userPromptSubmitInputSchema = hookCommonSchema.extend({
|
|
10
|
+
hook_event_name: z.literal("UserPromptSubmit"),
|
|
11
|
+
prompt: z.string()
|
|
12
|
+
});
|
|
13
|
+
var preToolUseInputSchema = hookCommonSchema.extend({
|
|
14
|
+
hook_event_name: z.literal("PreToolUse"),
|
|
15
|
+
tool_name: z.string(),
|
|
16
|
+
tool_input: z.record(z.string(), z.unknown()),
|
|
17
|
+
tool_use_id: z.string()
|
|
18
|
+
});
|
|
19
|
+
var postToolUseInputSchema = hookCommonSchema.extend({
|
|
20
|
+
hook_event_name: z.literal("PostToolUse"),
|
|
21
|
+
tool_name: z.string(),
|
|
22
|
+
tool_input: z.record(z.string(), z.unknown()),
|
|
23
|
+
tool_response: z.unknown().optional(),
|
|
24
|
+
tool_use_id: z.string()
|
|
25
|
+
});
|
|
26
|
+
var postToolUseFailureInputSchema = hookCommonSchema.extend({
|
|
27
|
+
hook_event_name: z.literal("PostToolUseFailure"),
|
|
28
|
+
tool_name: z.string(),
|
|
29
|
+
tool_input: z.record(z.string(), z.unknown()),
|
|
30
|
+
tool_use_id: z.string(),
|
|
31
|
+
error: z.string().optional(),
|
|
32
|
+
is_interrupt: z.boolean().optional()
|
|
33
|
+
});
|
|
34
|
+
var permissionRequestInputSchema = hookCommonSchema.extend({
|
|
35
|
+
hook_event_name: z.literal("PermissionRequest"),
|
|
36
|
+
tool_name: z.string(),
|
|
37
|
+
tool_input: z.record(z.string(), z.unknown()),
|
|
38
|
+
permission_suggestions: z.array(z.unknown()).optional()
|
|
39
|
+
});
|
|
40
|
+
var stopInputSchema = hookCommonSchema.extend({
|
|
41
|
+
hook_event_name: z.literal("Stop"),
|
|
42
|
+
stop_hook_active: z.boolean(),
|
|
43
|
+
last_assistant_message: z.string().optional()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/storage/counter.ts
|
|
47
|
+
import { readFile } from "fs/promises";
|
|
48
|
+
import { lock } from "proper-lockfile";
|
|
49
|
+
import writeFileAtomic from "write-file-atomic";
|
|
50
|
+
|
|
51
|
+
// src/schemas/counter.ts
|
|
52
|
+
import { z as z2 } from "zod/v4";
|
|
53
|
+
var counterSchema = z2.object({
|
|
54
|
+
total: z2.number().default(0),
|
|
55
|
+
session: z2.record(z2.string(), z2.number()).default({}),
|
|
56
|
+
last_analysis: z2.iso.datetime().optional(),
|
|
57
|
+
last_updated: z2.iso.datetime()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/storage/dirs.ts
|
|
61
|
+
import { mkdir } from "fs/promises";
|
|
62
|
+
import { join } from "path";
|
|
63
|
+
var BASE_DIR = join(process.env.HOME ?? "", ".harness-evolve");
|
|
64
|
+
var paths = {
|
|
65
|
+
base: BASE_DIR,
|
|
66
|
+
logs: {
|
|
67
|
+
prompts: join(BASE_DIR, "logs", "prompts"),
|
|
68
|
+
tools: join(BASE_DIR, "logs", "tools"),
|
|
69
|
+
permissions: join(BASE_DIR, "logs", "permissions"),
|
|
70
|
+
sessions: join(BASE_DIR, "logs", "sessions")
|
|
71
|
+
},
|
|
72
|
+
analysis: join(BASE_DIR, "analysis"),
|
|
73
|
+
analysisPreProcessed: join(BASE_DIR, "analysis", "pre-processed"),
|
|
74
|
+
summary: join(BASE_DIR, "analysis", "pre-processed", "summary.json"),
|
|
75
|
+
environmentSnapshot: join(BASE_DIR, "analysis", "environment-snapshot.json"),
|
|
76
|
+
analysisResult: join(BASE_DIR, "analysis", "analysis-result.json"),
|
|
77
|
+
pending: join(BASE_DIR, "pending"),
|
|
78
|
+
config: join(BASE_DIR, "config.json"),
|
|
79
|
+
counter: join(BASE_DIR, "counter.json"),
|
|
80
|
+
recommendations: join(BASE_DIR, "recommendations.md"),
|
|
81
|
+
recommendationState: join(BASE_DIR, "analysis", "recommendation-state.json"),
|
|
82
|
+
recommendationArchive: join(BASE_DIR, "analysis", "recommendations-archive"),
|
|
83
|
+
notificationFlag: join(BASE_DIR, "analysis", "has-pending-notifications"),
|
|
84
|
+
autoApplyLog: join(BASE_DIR, "analysis", "auto-apply-log.jsonl"),
|
|
85
|
+
outcomeHistory: join(BASE_DIR, "analysis", "outcome-history.jsonl")
|
|
86
|
+
};
|
|
87
|
+
var initialized = false;
|
|
88
|
+
async function ensureInit() {
|
|
89
|
+
if (initialized) return;
|
|
90
|
+
await mkdir(paths.logs.prompts, { recursive: true });
|
|
91
|
+
await mkdir(paths.logs.tools, { recursive: true });
|
|
92
|
+
await mkdir(paths.logs.permissions, { recursive: true });
|
|
93
|
+
await mkdir(paths.logs.sessions, { recursive: true });
|
|
94
|
+
await mkdir(paths.analysis, { recursive: true });
|
|
95
|
+
await mkdir(paths.analysisPreProcessed, { recursive: true });
|
|
96
|
+
await mkdir(paths.pending, { recursive: true });
|
|
97
|
+
await mkdir(paths.recommendationArchive, { recursive: true });
|
|
98
|
+
initialized = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/storage/counter.ts
|
|
102
|
+
async function readCounter() {
|
|
103
|
+
await ensureInit();
|
|
104
|
+
try {
|
|
105
|
+
const raw = await readFile(paths.counter, "utf-8");
|
|
106
|
+
return counterSchema.parse(JSON.parse(raw));
|
|
107
|
+
} catch {
|
|
108
|
+
return {
|
|
109
|
+
total: 0,
|
|
110
|
+
session: {},
|
|
111
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/storage/config.ts
|
|
117
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
118
|
+
import writeFileAtomic2 from "write-file-atomic";
|
|
119
|
+
|
|
120
|
+
// src/schemas/config.ts
|
|
121
|
+
import { z as z3 } from "zod/v4";
|
|
122
|
+
var configSchema = z3.object({
|
|
123
|
+
version: z3.number().default(1),
|
|
124
|
+
analysis: z3.object({
|
|
125
|
+
threshold: z3.number().min(1).default(50),
|
|
126
|
+
enabled: z3.boolean().default(true),
|
|
127
|
+
classifierThresholds: z3.record(z3.string(), z3.number()).default({})
|
|
128
|
+
}).default({ threshold: 50, enabled: true, classifierThresholds: {} }),
|
|
129
|
+
hooks: z3.object({
|
|
130
|
+
capturePrompts: z3.boolean().default(true),
|
|
131
|
+
captureTools: z3.boolean().default(true),
|
|
132
|
+
capturePermissions: z3.boolean().default(true),
|
|
133
|
+
captureSessions: z3.boolean().default(true)
|
|
134
|
+
}).default({
|
|
135
|
+
capturePrompts: true,
|
|
136
|
+
captureTools: true,
|
|
137
|
+
capturePermissions: true,
|
|
138
|
+
captureSessions: true
|
|
139
|
+
}),
|
|
140
|
+
scrubbing: z3.object({
|
|
141
|
+
enabled: z3.boolean().default(true),
|
|
142
|
+
highEntropyDetection: z3.boolean().default(false),
|
|
143
|
+
customPatterns: z3.array(z3.object({
|
|
144
|
+
name: z3.string(),
|
|
145
|
+
regex: z3.string(),
|
|
146
|
+
replacement: z3.string()
|
|
147
|
+
})).default([])
|
|
148
|
+
}).default({
|
|
149
|
+
enabled: true,
|
|
150
|
+
highEntropyDetection: false,
|
|
151
|
+
customPatterns: []
|
|
152
|
+
}),
|
|
153
|
+
delivery: z3.object({
|
|
154
|
+
stdoutInjection: z3.boolean().default(true),
|
|
155
|
+
maxTokens: z3.number().default(200),
|
|
156
|
+
fullAuto: z3.boolean().default(false),
|
|
157
|
+
maxRecommendationsInFile: z3.number().default(20),
|
|
158
|
+
archiveAfterDays: z3.number().default(7)
|
|
159
|
+
}).default({
|
|
160
|
+
stdoutInjection: true,
|
|
161
|
+
maxTokens: 200,
|
|
162
|
+
fullAuto: false,
|
|
163
|
+
maxRecommendationsInFile: 20,
|
|
164
|
+
archiveAfterDays: 7
|
|
165
|
+
})
|
|
166
|
+
}).strict();
|
|
167
|
+
|
|
168
|
+
// src/storage/config.ts
|
|
169
|
+
async function loadConfig() {
|
|
170
|
+
try {
|
|
171
|
+
const raw = await readFile2(paths.config, "utf-8");
|
|
172
|
+
return configSchema.parse(JSON.parse(raw));
|
|
173
|
+
} catch {
|
|
174
|
+
const defaults = configSchema.parse({});
|
|
175
|
+
await writeFileAtomic2(paths.config, JSON.stringify(defaults, null, 2));
|
|
176
|
+
return defaults;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/analysis/jsonl-reader.ts
|
|
181
|
+
import { createReadStream } from "fs";
|
|
182
|
+
import { readdir } from "fs/promises";
|
|
183
|
+
import { createInterface } from "readline";
|
|
184
|
+
import { join as join2 } from "path";
|
|
185
|
+
function formatDate(d) {
|
|
186
|
+
return d.toISOString().slice(0, 10);
|
|
187
|
+
}
|
|
188
|
+
async function readLogEntries(logDir, schema, options) {
|
|
189
|
+
let fileNames;
|
|
190
|
+
try {
|
|
191
|
+
fileNames = await readdir(logDir);
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
let jsonlFiles = fileNames.filter((f) => f.endsWith(".jsonl"));
|
|
196
|
+
const sinceStr = options?.since ? formatDate(options.since) : void 0;
|
|
197
|
+
const untilStr = options?.until ? formatDate(options.until) : void 0;
|
|
198
|
+
if (sinceStr || untilStr) {
|
|
199
|
+
jsonlFiles = jsonlFiles.filter((f) => {
|
|
200
|
+
const dateStr = f.replace(".jsonl", "");
|
|
201
|
+
if (sinceStr && dateStr < sinceStr) return false;
|
|
202
|
+
if (untilStr && dateStr > untilStr) return false;
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
jsonlFiles.sort();
|
|
207
|
+
const entries = [];
|
|
208
|
+
for (const file of jsonlFiles) {
|
|
209
|
+
const filePath = join2(logDir, file);
|
|
210
|
+
const rl = createInterface({
|
|
211
|
+
input: createReadStream(filePath, "utf-8"),
|
|
212
|
+
crlfDelay: Infinity
|
|
213
|
+
});
|
|
214
|
+
for await (const line of rl) {
|
|
215
|
+
if (!line.trim()) continue;
|
|
216
|
+
try {
|
|
217
|
+
const parsed = schema.parse(JSON.parse(line));
|
|
218
|
+
entries.push(parsed);
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return entries;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/analysis/schemas.ts
|
|
227
|
+
import { z as z4 } from "zod/v4";
|
|
228
|
+
var summarySchema = z4.object({
|
|
229
|
+
generated_at: z4.iso.datetime(),
|
|
230
|
+
period: z4.object({
|
|
231
|
+
since: z4.string(),
|
|
232
|
+
// YYYY-MM-DD
|
|
233
|
+
until: z4.string(),
|
|
234
|
+
// YYYY-MM-DD
|
|
235
|
+
days: z4.number()
|
|
236
|
+
}),
|
|
237
|
+
stats: z4.object({
|
|
238
|
+
total_prompts: z4.number(),
|
|
239
|
+
total_tool_uses: z4.number(),
|
|
240
|
+
total_permissions: z4.number(),
|
|
241
|
+
unique_sessions: z4.number()
|
|
242
|
+
}),
|
|
243
|
+
top_repeated_prompts: z4.array(
|
|
244
|
+
z4.object({
|
|
245
|
+
prompt: z4.string(),
|
|
246
|
+
count: z4.number(),
|
|
247
|
+
sessions: z4.number()
|
|
248
|
+
})
|
|
249
|
+
).max(20),
|
|
250
|
+
tool_frequency: z4.array(
|
|
251
|
+
z4.object({
|
|
252
|
+
tool_name: z4.string(),
|
|
253
|
+
count: z4.number(),
|
|
254
|
+
avg_duration_ms: z4.number().optional()
|
|
255
|
+
})
|
|
256
|
+
),
|
|
257
|
+
permission_patterns: z4.array(
|
|
258
|
+
z4.object({
|
|
259
|
+
tool_name: z4.string(),
|
|
260
|
+
count: z4.number(),
|
|
261
|
+
sessions: z4.number()
|
|
262
|
+
})
|
|
263
|
+
),
|
|
264
|
+
long_prompts: z4.array(
|
|
265
|
+
z4.object({
|
|
266
|
+
prompt_preview: z4.string(),
|
|
267
|
+
length: z4.number(),
|
|
268
|
+
count: z4.number()
|
|
269
|
+
})
|
|
270
|
+
).max(10)
|
|
271
|
+
});
|
|
272
|
+
var environmentSnapshotSchema = z4.object({
|
|
273
|
+
generated_at: z4.iso.datetime(),
|
|
274
|
+
claude_code: z4.object({
|
|
275
|
+
version: z4.string(),
|
|
276
|
+
version_known: z4.boolean(),
|
|
277
|
+
compatible: z4.boolean()
|
|
278
|
+
}),
|
|
279
|
+
settings: z4.object({
|
|
280
|
+
user: z4.unknown().nullable(),
|
|
281
|
+
project: z4.unknown().nullable(),
|
|
282
|
+
local: z4.unknown().nullable()
|
|
283
|
+
}),
|
|
284
|
+
installed_tools: z4.object({
|
|
285
|
+
plugins: z4.array(
|
|
286
|
+
z4.object({
|
|
287
|
+
name: z4.string(),
|
|
288
|
+
marketplace: z4.string(),
|
|
289
|
+
enabled: z4.boolean(),
|
|
290
|
+
scope: z4.string(),
|
|
291
|
+
capabilities: z4.array(z4.string())
|
|
292
|
+
})
|
|
293
|
+
),
|
|
294
|
+
skills: z4.array(
|
|
295
|
+
z4.object({
|
|
296
|
+
name: z4.string(),
|
|
297
|
+
scope: z4.enum(["user", "project"])
|
|
298
|
+
})
|
|
299
|
+
),
|
|
300
|
+
rules: z4.array(
|
|
301
|
+
z4.object({
|
|
302
|
+
name: z4.string(),
|
|
303
|
+
scope: z4.enum(["user", "project"])
|
|
304
|
+
})
|
|
305
|
+
),
|
|
306
|
+
hooks: z4.array(
|
|
307
|
+
z4.object({
|
|
308
|
+
event: z4.string(),
|
|
309
|
+
scope: z4.enum(["user", "project", "local"]),
|
|
310
|
+
type: z4.string()
|
|
311
|
+
})
|
|
312
|
+
),
|
|
313
|
+
claude_md: z4.array(
|
|
314
|
+
z4.object({
|
|
315
|
+
path: z4.string(),
|
|
316
|
+
exists: z4.boolean()
|
|
317
|
+
})
|
|
318
|
+
)
|
|
319
|
+
}),
|
|
320
|
+
detected_ecosystems: z4.array(z4.string())
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// src/schemas/log-entry.ts
|
|
324
|
+
import { z as z5 } from "zod/v4";
|
|
325
|
+
var promptEntrySchema = z5.object({
|
|
326
|
+
timestamp: z5.iso.datetime(),
|
|
327
|
+
session_id: z5.string(),
|
|
328
|
+
cwd: z5.string(),
|
|
329
|
+
prompt: z5.string(),
|
|
330
|
+
prompt_length: z5.number(),
|
|
331
|
+
transcript_path: z5.string().optional()
|
|
332
|
+
});
|
|
333
|
+
var toolEntrySchema = z5.object({
|
|
334
|
+
timestamp: z5.iso.datetime(),
|
|
335
|
+
session_id: z5.string(),
|
|
336
|
+
event: z5.enum(["pre", "post", "failure"]),
|
|
337
|
+
tool_name: z5.string(),
|
|
338
|
+
input_summary: z5.string().optional(),
|
|
339
|
+
duration_ms: z5.number().optional(),
|
|
340
|
+
success: z5.boolean().optional()
|
|
341
|
+
});
|
|
342
|
+
var permissionEntrySchema = z5.object({
|
|
343
|
+
timestamp: z5.iso.datetime(),
|
|
344
|
+
session_id: z5.string(),
|
|
345
|
+
tool_name: z5.string(),
|
|
346
|
+
decision: z5.enum(["approved", "denied", "unknown"])
|
|
347
|
+
});
|
|
348
|
+
var sessionEntrySchema = z5.object({
|
|
349
|
+
timestamp: z5.iso.datetime(),
|
|
350
|
+
session_id: z5.string(),
|
|
351
|
+
event: z5.enum(["start", "end"]),
|
|
352
|
+
cwd: z5.string().optional()
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// src/analysis/pre-processor.ts
|
|
356
|
+
import writeFileAtomic3 from "write-file-atomic";
|
|
357
|
+
var PROMPT_TRUNCATE_LEN = 100;
|
|
358
|
+
var LONG_PROMPT_THRESHOLD = 200;
|
|
359
|
+
var DEFAULT_TOP_N = 20;
|
|
360
|
+
var DEFAULT_DAYS = 30;
|
|
361
|
+
var MAX_LONG_PROMPTS = 10;
|
|
362
|
+
function normalizePrompt(prompt) {
|
|
363
|
+
return prompt.trim().toLowerCase().replace(/\s+/g, " ");
|
|
364
|
+
}
|
|
365
|
+
function formatDate2(d) {
|
|
366
|
+
return d.toISOString().slice(0, 10);
|
|
367
|
+
}
|
|
368
|
+
function countWithSessions(items) {
|
|
369
|
+
const map = /* @__PURE__ */ new Map();
|
|
370
|
+
for (const { key, session } of items) {
|
|
371
|
+
const existing = map.get(key);
|
|
372
|
+
if (existing) {
|
|
373
|
+
existing.count += 1;
|
|
374
|
+
existing.sessions.add(session);
|
|
375
|
+
} else {
|
|
376
|
+
map.set(key, { count: 1, sessions: /* @__PURE__ */ new Set([session]) });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return map;
|
|
380
|
+
}
|
|
381
|
+
function computeToolFrequency(tools) {
|
|
382
|
+
const map = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const entry of tools) {
|
|
384
|
+
const existing = map.get(entry.tool_name);
|
|
385
|
+
if (existing) {
|
|
386
|
+
existing.count += 1;
|
|
387
|
+
if (entry.event === "post" && entry.duration_ms != null) {
|
|
388
|
+
existing.durations.push(entry.duration_ms);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const durations = [];
|
|
392
|
+
if (entry.event === "post" && entry.duration_ms != null) {
|
|
393
|
+
durations.push(entry.duration_ms);
|
|
394
|
+
}
|
|
395
|
+
map.set(entry.tool_name, { count: 1, durations });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return Array.from(map.entries()).map(([tool_name, { count, durations }]) => ({
|
|
399
|
+
tool_name,
|
|
400
|
+
count,
|
|
401
|
+
avg_duration_ms: durations.length > 0 ? Math.round(
|
|
402
|
+
durations.reduce((sum, d) => sum + d, 0) / durations.length
|
|
403
|
+
) : void 0
|
|
404
|
+
})).sort((a, b) => b.count - a.count);
|
|
405
|
+
}
|
|
406
|
+
function detectLongPrompts(prompts) {
|
|
407
|
+
const map = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const entry of prompts) {
|
|
409
|
+
const words = entry.prompt.trim().split(/\s+/);
|
|
410
|
+
if (words.length <= LONG_PROMPT_THRESHOLD) continue;
|
|
411
|
+
const key = normalizePrompt(entry.prompt);
|
|
412
|
+
const existing = map.get(key);
|
|
413
|
+
if (existing) {
|
|
414
|
+
existing.count += 1;
|
|
415
|
+
} else {
|
|
416
|
+
map.set(key, { length: words.length, count: 1 });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return Array.from(map.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, MAX_LONG_PROMPTS).map(([normalized, { length, count }]) => ({
|
|
420
|
+
prompt_preview: normalized.slice(0, PROMPT_TRUNCATE_LEN),
|
|
421
|
+
length,
|
|
422
|
+
count
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
async function preProcess(options) {
|
|
426
|
+
const until = options?.until ?? /* @__PURE__ */ new Date();
|
|
427
|
+
const since = options?.since ?? new Date(until.getTime() - DEFAULT_DAYS * 864e5);
|
|
428
|
+
const topN = options?.topN ?? DEFAULT_TOP_N;
|
|
429
|
+
const [prompts, tools, permissions] = await Promise.all([
|
|
430
|
+
readLogEntries(paths.logs.prompts, promptEntrySchema, { since, until }),
|
|
431
|
+
readLogEntries(paths.logs.tools, toolEntrySchema, { since, until }),
|
|
432
|
+
readLogEntries(paths.logs.permissions, permissionEntrySchema, {
|
|
433
|
+
since,
|
|
434
|
+
until
|
|
435
|
+
})
|
|
436
|
+
]);
|
|
437
|
+
const sessionSet = /* @__PURE__ */ new Set();
|
|
438
|
+
for (const p of prompts) sessionSet.add(p.session_id);
|
|
439
|
+
for (const t of tools) sessionSet.add(t.session_id);
|
|
440
|
+
for (const perm of permissions) sessionSet.add(perm.session_id);
|
|
441
|
+
const promptCounts = countWithSessions(
|
|
442
|
+
prompts.map((p) => ({
|
|
443
|
+
key: normalizePrompt(p.prompt),
|
|
444
|
+
session: p.session_id
|
|
445
|
+
}))
|
|
446
|
+
);
|
|
447
|
+
const topRepeatedPrompts = Array.from(promptCounts.entries()).sort((a, b) => b[1].count - a[1].count).slice(0, topN).map(([key, { count, sessions }]) => ({
|
|
448
|
+
prompt: key.slice(0, PROMPT_TRUNCATE_LEN),
|
|
449
|
+
count,
|
|
450
|
+
sessions: sessions.size
|
|
451
|
+
}));
|
|
452
|
+
const toolFrequency = computeToolFrequency(tools);
|
|
453
|
+
const permissionCounts = countWithSessions(
|
|
454
|
+
permissions.map((p) => ({
|
|
455
|
+
key: p.tool_name,
|
|
456
|
+
session: p.session_id
|
|
457
|
+
}))
|
|
458
|
+
);
|
|
459
|
+
const permissionPatterns = Array.from(permissionCounts.entries()).sort((a, b) => b[1].count - a[1].count).map(([tool_name, { count, sessions }]) => ({
|
|
460
|
+
tool_name,
|
|
461
|
+
count,
|
|
462
|
+
sessions: sessions.size
|
|
463
|
+
}));
|
|
464
|
+
const longPrompts = detectLongPrompts(prompts);
|
|
465
|
+
const summary = summarySchema.parse({
|
|
466
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
467
|
+
period: {
|
|
468
|
+
since: formatDate2(since),
|
|
469
|
+
until: formatDate2(until),
|
|
470
|
+
days: DEFAULT_DAYS
|
|
471
|
+
},
|
|
472
|
+
stats: {
|
|
473
|
+
total_prompts: prompts.length,
|
|
474
|
+
total_tool_uses: tools.length,
|
|
475
|
+
total_permissions: permissions.length,
|
|
476
|
+
unique_sessions: sessionSet.size
|
|
477
|
+
},
|
|
478
|
+
top_repeated_prompts: topRepeatedPrompts,
|
|
479
|
+
tool_frequency: toolFrequency,
|
|
480
|
+
permission_patterns: permissionPatterns,
|
|
481
|
+
long_prompts: longPrompts
|
|
482
|
+
});
|
|
483
|
+
await ensureInit();
|
|
484
|
+
await writeFileAtomic3(paths.summary, JSON.stringify(summary));
|
|
485
|
+
return summary;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/analysis/environment-scanner.ts
|
|
489
|
+
import { readdir as readdir2, readFile as readFile3, access } from "fs/promises";
|
|
490
|
+
import { execFileSync } from "child_process";
|
|
491
|
+
import { join as join3 } from "path";
|
|
492
|
+
import { constants } from "fs";
|
|
493
|
+
import writeFileAtomic4 from "write-file-atomic";
|
|
494
|
+
var KNOWN_COMPATIBLE_MIN = "2.1.0";
|
|
495
|
+
var KNOWN_COMPATIBLE_MAX = "2.1.99";
|
|
496
|
+
async function scanEnvironment(cwd, home) {
|
|
497
|
+
const homeDir = home ?? process.env.HOME ?? "";
|
|
498
|
+
const [userSettings, projectSettings, localSettings] = await Promise.all([
|
|
499
|
+
readSettingsSafe(join3(homeDir, ".claude", "settings.json")),
|
|
500
|
+
readSettingsSafe(join3(cwd, ".claude", "settings.json")),
|
|
501
|
+
readSettingsSafe(join3(cwd, ".claude", "settings.local.json"))
|
|
502
|
+
]);
|
|
503
|
+
const enabledPluginNames = extractEnabledPlugins(userSettings);
|
|
504
|
+
const [claudeVersion, plugins, skills, rules, hooks, claudeMds, ecosystems] = await Promise.all([
|
|
505
|
+
Promise.resolve(detectClaudeCodeVersion()),
|
|
506
|
+
discoverPlugins(homeDir, enabledPluginNames),
|
|
507
|
+
discoverSkills(homeDir, cwd),
|
|
508
|
+
discoverRules(cwd),
|
|
509
|
+
Promise.resolve(
|
|
510
|
+
discoverHooks(userSettings, projectSettings, localSettings)
|
|
511
|
+
),
|
|
512
|
+
discoverClaudeMd(homeDir, cwd),
|
|
513
|
+
detectEcosystems(cwd, homeDir)
|
|
514
|
+
]);
|
|
515
|
+
const snapshot = {
|
|
516
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
517
|
+
claude_code: claudeVersion,
|
|
518
|
+
settings: {
|
|
519
|
+
user: userSettings,
|
|
520
|
+
project: projectSettings,
|
|
521
|
+
local: localSettings
|
|
522
|
+
},
|
|
523
|
+
installed_tools: {
|
|
524
|
+
plugins,
|
|
525
|
+
skills,
|
|
526
|
+
rules,
|
|
527
|
+
hooks,
|
|
528
|
+
claude_md: claudeMds
|
|
529
|
+
},
|
|
530
|
+
detected_ecosystems: ecosystems
|
|
531
|
+
};
|
|
532
|
+
const validated = environmentSnapshotSchema.parse(snapshot);
|
|
533
|
+
await ensureInit();
|
|
534
|
+
await writeFileAtomic4(
|
|
535
|
+
paths.environmentSnapshot,
|
|
536
|
+
JSON.stringify(validated)
|
|
537
|
+
);
|
|
538
|
+
return validated;
|
|
539
|
+
}
|
|
540
|
+
function detectClaudeCodeVersion() {
|
|
541
|
+
try {
|
|
542
|
+
const output = execFileSync("claude", ["--version"], {
|
|
543
|
+
timeout: 3e3,
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
546
|
+
}).trim();
|
|
547
|
+
const match = output.match(/^(\d+\.\d+\.\d+)/);
|
|
548
|
+
if (!match) {
|
|
549
|
+
return { version: "unknown", version_known: false, compatible: false };
|
|
550
|
+
}
|
|
551
|
+
const version = match[1];
|
|
552
|
+
const compatible = compareSemver(version, KNOWN_COMPATIBLE_MIN) >= 0 && compareSemver(version, KNOWN_COMPATIBLE_MAX) <= 0;
|
|
553
|
+
return { version, version_known: true, compatible };
|
|
554
|
+
} catch {
|
|
555
|
+
return { version: "unknown", version_known: false, compatible: false };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function readSettingsSafe(filePath) {
|
|
559
|
+
try {
|
|
560
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
561
|
+
return JSON.parse(raw);
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function extractEnabledPlugins(settings) {
|
|
567
|
+
if (!settings || typeof settings !== "object") return [];
|
|
568
|
+
const obj = settings;
|
|
569
|
+
if (!Array.isArray(obj.enabledPlugins)) return [];
|
|
570
|
+
return obj.enabledPlugins.map((p) => {
|
|
571
|
+
if (typeof p === "string") return p;
|
|
572
|
+
if (p && typeof p === "object" && "name" in p) {
|
|
573
|
+
return String(p.name);
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}).filter((n) => n !== null);
|
|
577
|
+
}
|
|
578
|
+
async function discoverPlugins(home, enabledPluginNames) {
|
|
579
|
+
try {
|
|
580
|
+
const pluginsFile = join3(home, ".claude", "plugins", "installed_plugins.json");
|
|
581
|
+
const raw = await readFile3(pluginsFile, "utf-8");
|
|
582
|
+
const installed = JSON.parse(raw);
|
|
583
|
+
if (!Array.isArray(installed)) return [];
|
|
584
|
+
const plugins = [];
|
|
585
|
+
for (const entry of installed) {
|
|
586
|
+
if (!entry || typeof entry !== "object") continue;
|
|
587
|
+
const plugin = entry;
|
|
588
|
+
const name = String(plugin.name ?? "");
|
|
589
|
+
const marketplace = String(plugin.marketplace ?? "unknown");
|
|
590
|
+
const scope = String(plugin.scope ?? "user");
|
|
591
|
+
const version = String(plugin.version ?? "latest");
|
|
592
|
+
const enabled = enabledPluginNames.includes(name);
|
|
593
|
+
const capabilities = await scanPluginCapabilities(
|
|
594
|
+
home,
|
|
595
|
+
marketplace,
|
|
596
|
+
name,
|
|
597
|
+
version
|
|
598
|
+
);
|
|
599
|
+
plugins.push({ name, marketplace, enabled, scope, capabilities });
|
|
600
|
+
}
|
|
601
|
+
return plugins;
|
|
602
|
+
} catch {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function scanPluginCapabilities(home, marketplace, pluginName, version) {
|
|
607
|
+
const knownCapabilities = ["commands", "skills", "hooks", "agents"];
|
|
608
|
+
const capabilities = [];
|
|
609
|
+
try {
|
|
610
|
+
const cacheDir = join3(
|
|
611
|
+
home,
|
|
612
|
+
".claude",
|
|
613
|
+
"plugins",
|
|
614
|
+
"cache",
|
|
615
|
+
marketplace,
|
|
616
|
+
pluginName,
|
|
617
|
+
version
|
|
618
|
+
);
|
|
619
|
+
const entries = await readdir2(cacheDir, { withFileTypes: true });
|
|
620
|
+
for (const entry of entries) {
|
|
621
|
+
if (entry.isDirectory() && knownCapabilities.includes(entry.name)) {
|
|
622
|
+
capabilities.push(entry.name);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
return capabilities;
|
|
628
|
+
}
|
|
629
|
+
async function discoverSkills(home, cwd) {
|
|
630
|
+
const skills = [];
|
|
631
|
+
try {
|
|
632
|
+
const userSkillsDir = join3(home, ".claude", "skills");
|
|
633
|
+
const entries = await readdir2(userSkillsDir, { withFileTypes: true });
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
if (entry.isDirectory()) {
|
|
636
|
+
skills.push({ name: entry.name, scope: "user" });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const projectSkillsDir = join3(cwd, ".claude", "skills");
|
|
643
|
+
const entries = await readdir2(projectSkillsDir, { withFileTypes: true });
|
|
644
|
+
for (const entry of entries) {
|
|
645
|
+
if (entry.isDirectory()) {
|
|
646
|
+
skills.push({ name: entry.name, scope: "project" });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
return skills;
|
|
652
|
+
}
|
|
653
|
+
async function discoverRules(cwd) {
|
|
654
|
+
const rules = [];
|
|
655
|
+
try {
|
|
656
|
+
const rulesDir = join3(cwd, ".claude", "rules");
|
|
657
|
+
const entries = await readdir2(rulesDir, { withFileTypes: true });
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
if (entry.isDirectory()) {
|
|
660
|
+
rules.push({ name: entry.name, scope: "project" });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
return rules;
|
|
666
|
+
}
|
|
667
|
+
function discoverHooks(userSettings, projectSettings, localSettings) {
|
|
668
|
+
const hooks = [];
|
|
669
|
+
extractHooksFromSettings(userSettings, "user", hooks);
|
|
670
|
+
extractHooksFromSettings(projectSettings, "project", hooks);
|
|
671
|
+
extractHooksFromSettings(localSettings, "local", hooks);
|
|
672
|
+
return hooks;
|
|
673
|
+
}
|
|
674
|
+
function extractHooksFromSettings(settings, scope, hooks) {
|
|
675
|
+
if (!settings || typeof settings !== "object") return;
|
|
676
|
+
const obj = settings;
|
|
677
|
+
if (!obj.hooks || typeof obj.hooks !== "object") return;
|
|
678
|
+
const hooksConfig = obj.hooks;
|
|
679
|
+
for (const [event, defs] of Object.entries(hooksConfig)) {
|
|
680
|
+
if (!Array.isArray(defs)) continue;
|
|
681
|
+
for (const def of defs) {
|
|
682
|
+
if (!def || typeof def !== "object") continue;
|
|
683
|
+
const hookDef = def;
|
|
684
|
+
const type = String(hookDef.type ?? "command");
|
|
685
|
+
hooks.push({ event, scope, type });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function discoverClaudeMd(home, cwd) {
|
|
690
|
+
const locations = [
|
|
691
|
+
join3(cwd, "CLAUDE.md"),
|
|
692
|
+
join3(cwd, ".claude", "CLAUDE.md"),
|
|
693
|
+
join3(home, ".claude", "CLAUDE.md")
|
|
694
|
+
];
|
|
695
|
+
const results = [];
|
|
696
|
+
for (const path of locations) {
|
|
697
|
+
let exists = false;
|
|
698
|
+
try {
|
|
699
|
+
await access(path, constants.F_OK);
|
|
700
|
+
exists = true;
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
results.push({ path, exists });
|
|
704
|
+
}
|
|
705
|
+
return results;
|
|
706
|
+
}
|
|
707
|
+
async function detectEcosystems(cwd, home) {
|
|
708
|
+
const ecosystems = [];
|
|
709
|
+
try {
|
|
710
|
+
await access(join3(cwd, ".planning"), constants.F_OK);
|
|
711
|
+
ecosystems.push("gsd");
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const skillsDir = join3(home, ".claude", "skills");
|
|
716
|
+
const entries = await readdir2(skillsDir);
|
|
717
|
+
if (entries.some((e) => e.toLowerCase().includes("cog"))) {
|
|
718
|
+
ecosystems.push("cog");
|
|
719
|
+
}
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
return ecosystems;
|
|
723
|
+
}
|
|
724
|
+
function compareSemver(a, b) {
|
|
725
|
+
const partsA = a.split(".").map(Number);
|
|
726
|
+
const partsB = b.split(".").map(Number);
|
|
727
|
+
for (let i = 0; i < 3; i++) {
|
|
728
|
+
const numA = partsA[i] ?? 0;
|
|
729
|
+
const numB = partsB[i] ?? 0;
|
|
730
|
+
if (numA < numB) return -1;
|
|
731
|
+
if (numA > numB) return 1;
|
|
732
|
+
}
|
|
733
|
+
return 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/analysis/classifiers/repeated-prompts.ts
|
|
737
|
+
function truncate(str, maxLen) {
|
|
738
|
+
if (str.length <= maxLen) return str;
|
|
739
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
740
|
+
}
|
|
741
|
+
function classifyRepeatedPrompts(summary, _snapshot, config) {
|
|
742
|
+
const recommendations = [];
|
|
743
|
+
const threshold = config.thresholds.repeated_prompt_min_count;
|
|
744
|
+
for (let i = 0; i < summary.top_repeated_prompts.length; i++) {
|
|
745
|
+
const entry = summary.top_repeated_prompts[i];
|
|
746
|
+
if (entry.count < threshold) continue;
|
|
747
|
+
const wordCount = entry.prompt.split(/\s+/).length;
|
|
748
|
+
if (wordCount > 50) continue;
|
|
749
|
+
const confidence = entry.count >= config.thresholds.repeated_prompt_high_count && entry.sessions >= config.thresholds.repeated_prompt_high_sessions ? "HIGH" : "MEDIUM";
|
|
750
|
+
const truncatedPrompt = truncate(entry.prompt, 60);
|
|
751
|
+
recommendations.push({
|
|
752
|
+
id: `rec-repeated-${i}`,
|
|
753
|
+
target: "HOOK",
|
|
754
|
+
confidence,
|
|
755
|
+
pattern_type: "repeated_prompt",
|
|
756
|
+
title: `Repeated prompt: "${truncatedPrompt}"`,
|
|
757
|
+
description: `This prompt has been used ${entry.count} times across ${entry.sessions} sessions. Consider creating a hook or alias to automate this.`,
|
|
758
|
+
evidence: {
|
|
759
|
+
count: entry.count,
|
|
760
|
+
sessions: entry.sessions,
|
|
761
|
+
examples: [entry.prompt]
|
|
762
|
+
},
|
|
763
|
+
suggested_action: `Create a UserPromptSubmit hook that detects "${truncatedPrompt}" and auto-executes the intended action.`
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
return recommendations;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/analysis/classifiers/long-prompts.ts
|
|
770
|
+
function classifyLongPrompts(summary, _snapshot, config) {
|
|
771
|
+
const recommendations = [];
|
|
772
|
+
for (let i = 0; i < summary.long_prompts.length; i++) {
|
|
773
|
+
const entry = summary.long_prompts[i];
|
|
774
|
+
if (entry.length < config.thresholds.long_prompt_min_words) continue;
|
|
775
|
+
if (entry.count < config.thresholds.long_prompt_min_count) continue;
|
|
776
|
+
const confidence = entry.count >= config.thresholds.long_prompt_high_count && entry.length >= config.thresholds.long_prompt_high_words ? "HIGH" : "MEDIUM";
|
|
777
|
+
recommendations.push({
|
|
778
|
+
id: `rec-long-${i}`,
|
|
779
|
+
target: "SKILL",
|
|
780
|
+
confidence,
|
|
781
|
+
pattern_type: "long_prompt",
|
|
782
|
+
title: `Long repeated prompt (${entry.length} words, ${entry.count}x)`,
|
|
783
|
+
description: `A ${entry.length}-word prompt has been used ${entry.count} times. Consider converting it to a reusable skill.`,
|
|
784
|
+
evidence: {
|
|
785
|
+
count: entry.count,
|
|
786
|
+
examples: [entry.prompt_preview]
|
|
787
|
+
},
|
|
788
|
+
suggested_action: "Create a skill in .claude/skills/ that encapsulates this prompt as a reusable workflow."
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return recommendations;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/analysis/classifiers/permission-patterns.ts
|
|
795
|
+
function classifyPermissionPatterns(summary, _snapshot, config) {
|
|
796
|
+
const recommendations = [];
|
|
797
|
+
for (let i = 0; i < summary.permission_patterns.length; i++) {
|
|
798
|
+
const entry = summary.permission_patterns[i];
|
|
799
|
+
if (entry.count < config.thresholds.permission_approval_min_count) continue;
|
|
800
|
+
if (entry.sessions < config.thresholds.permission_approval_min_sessions) continue;
|
|
801
|
+
const confidence = entry.count >= config.thresholds.permission_approval_high_count && entry.sessions >= config.thresholds.permission_approval_high_sessions ? "HIGH" : "MEDIUM";
|
|
802
|
+
recommendations.push({
|
|
803
|
+
id: `rec-permission-always-approved-${i}`,
|
|
804
|
+
target: "SETTINGS",
|
|
805
|
+
confidence,
|
|
806
|
+
pattern_type: "permission-always-approved",
|
|
807
|
+
title: `Frequently approved tool: ${entry.tool_name}`,
|
|
808
|
+
description: `You have approved "${entry.tool_name}" ${entry.count} times across ${entry.sessions} sessions. Consider adding it to allowedTools in settings.json.`,
|
|
809
|
+
evidence: {
|
|
810
|
+
count: entry.count,
|
|
811
|
+
sessions: entry.sessions,
|
|
812
|
+
examples: [`${entry.tool_name} approved ${entry.count} times`]
|
|
813
|
+
},
|
|
814
|
+
suggested_action: `Add "${entry.tool_name}" to the "allow" array in ~/.claude/settings.json permissions.`
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
return recommendations;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/analysis/classifiers/code-corrections.ts
|
|
821
|
+
var CODE_MODIFICATION_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
|
|
822
|
+
var HIGH_USAGE_THRESHOLD = 20;
|
|
823
|
+
function classifyCodeCorrections(summary, _snapshot, _config) {
|
|
824
|
+
const recommendations = [];
|
|
825
|
+
let index = 0;
|
|
826
|
+
for (const entry of summary.tool_frequency) {
|
|
827
|
+
if (!CODE_MODIFICATION_TOOLS.has(entry.tool_name)) continue;
|
|
828
|
+
if (entry.count < HIGH_USAGE_THRESHOLD) continue;
|
|
829
|
+
recommendations.push({
|
|
830
|
+
id: `rec-correction-${index}`,
|
|
831
|
+
target: "RULE",
|
|
832
|
+
confidence: "LOW",
|
|
833
|
+
pattern_type: "code_correction",
|
|
834
|
+
title: `Frequent code modifications with ${entry.tool_name} (${entry.count} uses)`,
|
|
835
|
+
description: `The ${entry.tool_name} tool has been used ${entry.count} times. Review for recurring patterns that could become a coding rule or convention.`,
|
|
836
|
+
evidence: {
|
|
837
|
+
count: entry.count,
|
|
838
|
+
examples: [entry.tool_name]
|
|
839
|
+
},
|
|
840
|
+
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/.`
|
|
841
|
+
});
|
|
842
|
+
index++;
|
|
843
|
+
}
|
|
844
|
+
return recommendations;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/analysis/classifiers/personal-info.ts
|
|
848
|
+
var PERSONAL_KEYWORDS = [
|
|
849
|
+
"my name is",
|
|
850
|
+
"i live in",
|
|
851
|
+
"i work at",
|
|
852
|
+
"i prefer",
|
|
853
|
+
"my email",
|
|
854
|
+
"my project",
|
|
855
|
+
"always use",
|
|
856
|
+
"never use"
|
|
857
|
+
];
|
|
858
|
+
var MIN_COUNT = 2;
|
|
859
|
+
function classifyPersonalInfo(summary, _snapshot, _config) {
|
|
860
|
+
const recommendations = [];
|
|
861
|
+
const matchedKeywords = /* @__PURE__ */ new Set();
|
|
862
|
+
let index = 0;
|
|
863
|
+
for (const entry of summary.top_repeated_prompts) {
|
|
864
|
+
if (entry.count < MIN_COUNT) continue;
|
|
865
|
+
const lowerPrompt = entry.prompt.toLowerCase();
|
|
866
|
+
for (const keyword of PERSONAL_KEYWORDS) {
|
|
867
|
+
if (matchedKeywords.has(keyword)) continue;
|
|
868
|
+
if (!lowerPrompt.includes(keyword)) continue;
|
|
869
|
+
matchedKeywords.add(keyword);
|
|
870
|
+
recommendations.push({
|
|
871
|
+
id: `rec-personal-${index}`,
|
|
872
|
+
target: "MEMORY",
|
|
873
|
+
confidence: "LOW",
|
|
874
|
+
pattern_type: "personal_info",
|
|
875
|
+
title: `Personal preference detected: "${keyword}..."`,
|
|
876
|
+
description: `A prompt mentioning personal information ("${keyword}") has appeared ${entry.count} times. Consider storing this in memory for automatic context.`,
|
|
877
|
+
evidence: {
|
|
878
|
+
count: entry.count,
|
|
879
|
+
sessions: entry.sessions,
|
|
880
|
+
examples: [entry.prompt]
|
|
881
|
+
},
|
|
882
|
+
suggested_action: "Add this information to memory (e.g., CLAUDE.md or a memory file) so Claude Code can use it without being reminded."
|
|
883
|
+
});
|
|
884
|
+
index++;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return recommendations;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/analysis/classifiers/config-drift.ts
|
|
891
|
+
var MAX_HOOKS_BEFORE_REVIEW = 10;
|
|
892
|
+
function classifyConfigDrift(_summary, snapshot, _config) {
|
|
893
|
+
const recommendations = [];
|
|
894
|
+
let index = 0;
|
|
895
|
+
const hookEvents = new Set(snapshot.installed_tools.hooks.map((h) => h.event));
|
|
896
|
+
const ruleNames = new Set(snapshot.installed_tools.rules.map((r) => r.name));
|
|
897
|
+
for (const overlap of hookEvents) {
|
|
898
|
+
if (!ruleNames.has(overlap)) continue;
|
|
899
|
+
recommendations.push({
|
|
900
|
+
id: `rec-drift-${index}`,
|
|
901
|
+
target: "RULE",
|
|
902
|
+
confidence: "LOW",
|
|
903
|
+
pattern_type: "config_drift",
|
|
904
|
+
title: `Hook-rule overlap detected: "${overlap}"`,
|
|
905
|
+
description: `Both a hook (event: "${overlap}") and a rule (name: "${overlap}") exist. This may indicate duplicated behavior that should be consolidated into one mechanism.`,
|
|
906
|
+
evidence: {
|
|
907
|
+
count: 2,
|
|
908
|
+
examples: [`Hook event: ${overlap}`, `Rule name: ${overlap}`]
|
|
909
|
+
},
|
|
910
|
+
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).`
|
|
911
|
+
});
|
|
912
|
+
index++;
|
|
913
|
+
}
|
|
914
|
+
const existingClaudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists);
|
|
915
|
+
if (existingClaudeMd.length > 1) {
|
|
916
|
+
recommendations.push({
|
|
917
|
+
id: `rec-drift-${index}`,
|
|
918
|
+
target: "CLAUDE_MD",
|
|
919
|
+
confidence: "LOW",
|
|
920
|
+
pattern_type: "config_drift",
|
|
921
|
+
title: `Multiple CLAUDE.md files detected (${existingClaudeMd.length})`,
|
|
922
|
+
description: `Found ${existingClaudeMd.length} existing CLAUDE.md files. Multiple CLAUDE.md files may contain contradictory instructions. Review for consistency.`,
|
|
923
|
+
evidence: {
|
|
924
|
+
count: existingClaudeMd.length,
|
|
925
|
+
examples: existingClaudeMd.slice(0, 3).map((c) => c.path)
|
|
926
|
+
},
|
|
927
|
+
suggested_action: "Review all CLAUDE.md files for contradictions or redundancies. Consider consolidating shared instructions into the most appropriate scope."
|
|
928
|
+
});
|
|
929
|
+
index++;
|
|
930
|
+
}
|
|
931
|
+
if (snapshot.installed_tools.hooks.length > MAX_HOOKS_BEFORE_REVIEW) {
|
|
932
|
+
recommendations.push({
|
|
933
|
+
id: `rec-drift-${index}`,
|
|
934
|
+
target: "HOOK",
|
|
935
|
+
confidence: "LOW",
|
|
936
|
+
pattern_type: "config_drift",
|
|
937
|
+
title: `Excessive hook count (${snapshot.installed_tools.hooks.length} hooks)`,
|
|
938
|
+
description: `Found ${snapshot.installed_tools.hooks.length} hooks across all scopes. This many hooks may indicate redundancy or performance concerns. Review for consolidation.`,
|
|
939
|
+
evidence: {
|
|
940
|
+
count: snapshot.installed_tools.hooks.length,
|
|
941
|
+
examples: snapshot.installed_tools.hooks.slice(0, 3).map((h) => `${h.event} (${h.scope})`)
|
|
942
|
+
},
|
|
943
|
+
suggested_action: "Review all hooks for overlapping functionality. Consider combining hooks that trigger on the same event or serve similar purposes."
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return recommendations;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/analysis/classifiers/ecosystem-adapter.ts
|
|
950
|
+
var MULTI_STEP_MIN_COUNT = 3;
|
|
951
|
+
function classifyEcosystemAdaptations(summary, snapshot, _config) {
|
|
952
|
+
const recommendations = [];
|
|
953
|
+
let index = 0;
|
|
954
|
+
if (snapshot.claude_code.version_known && !snapshot.claude_code.compatible && snapshot.claude_code.version !== "unknown") {
|
|
955
|
+
recommendations.push({
|
|
956
|
+
id: `rec-ecosystem-${index}`,
|
|
957
|
+
target: "CLAUDE_MD",
|
|
958
|
+
confidence: "MEDIUM",
|
|
959
|
+
pattern_type: "version_update",
|
|
960
|
+
title: `Claude Code version ${snapshot.claude_code.version} detected (outside tested range)`,
|
|
961
|
+
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.`,
|
|
962
|
+
evidence: {
|
|
963
|
+
count: 1,
|
|
964
|
+
examples: [`Version: ${snapshot.claude_code.version}`]
|
|
965
|
+
},
|
|
966
|
+
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.`
|
|
967
|
+
});
|
|
968
|
+
index++;
|
|
969
|
+
}
|
|
970
|
+
if (snapshot.detected_ecosystems.includes("gsd")) {
|
|
971
|
+
const multiStepPrompts = summary.top_repeated_prompts.filter(
|
|
972
|
+
(p) => p.count >= MULTI_STEP_MIN_COUNT
|
|
973
|
+
);
|
|
974
|
+
if (multiStepPrompts.length > 0) {
|
|
975
|
+
const topPrompt = multiStepPrompts[0];
|
|
976
|
+
recommendations.push({
|
|
977
|
+
id: `rec-ecosystem-${index}`,
|
|
978
|
+
target: "SKILL",
|
|
979
|
+
confidence: "LOW",
|
|
980
|
+
pattern_type: "ecosystem_gsd",
|
|
981
|
+
title: "GSD workflow detected -- consider /gsd slash commands",
|
|
982
|
+
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.",
|
|
983
|
+
evidence: {
|
|
984
|
+
count: multiStepPrompts.length,
|
|
985
|
+
examples: [topPrompt.prompt]
|
|
986
|
+
},
|
|
987
|
+
suggested_action: "Review repeated prompts and consider if they map to GSD phases (/gsd:plan-phase, /gsd:execute-phase) or custom slash commands.",
|
|
988
|
+
ecosystem_context: "GSD detected: Use /gsd slash commands and .planning patterns for multi-step workflows instead of standalone skills"
|
|
989
|
+
});
|
|
990
|
+
index++;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (snapshot.detected_ecosystems.includes("cog")) {
|
|
994
|
+
recommendations.push({
|
|
995
|
+
id: `rec-ecosystem-${index}`,
|
|
996
|
+
target: "MEMORY",
|
|
997
|
+
confidence: "LOW",
|
|
998
|
+
pattern_type: "ecosystem_cog",
|
|
999
|
+
title: "Cog memory system detected -- route memory to Cog tiers",
|
|
1000
|
+
description: "Cog is installed. Personal information and contextual preferences should be routed to Cog's tiered memory system rather than raw CLAUDE.md entries.",
|
|
1001
|
+
evidence: {
|
|
1002
|
+
count: 1,
|
|
1003
|
+
examples: ["Cog detected in ~/.claude/skills/"]
|
|
1004
|
+
},
|
|
1005
|
+
suggested_action: "Use /reflect and /evolve Cog commands for memory management instead of manually editing CLAUDE.md.",
|
|
1006
|
+
ecosystem_context: "Cog detected: Route memory entries to Cog tiers (/reflect, /evolve) instead of raw CLAUDE.md"
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return recommendations;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/analysis/experience-level.ts
|
|
1013
|
+
function computeExperienceLevel(snapshot) {
|
|
1014
|
+
const hooks = snapshot.installed_tools.hooks.length;
|
|
1015
|
+
const rules = snapshot.installed_tools.rules.length;
|
|
1016
|
+
const skills = snapshot.installed_tools.skills.length;
|
|
1017
|
+
const plugins = snapshot.installed_tools.plugins.length;
|
|
1018
|
+
const claudeMd = snapshot.installed_tools.claude_md.filter((c) => c.exists).length;
|
|
1019
|
+
const ecosystems = snapshot.detected_ecosystems.length;
|
|
1020
|
+
const score = Math.min(
|
|
1021
|
+
100,
|
|
1022
|
+
hooks * 8 + rules * 6 + skills * 5 + plugins * 10 + claudeMd * 3 + ecosystems * 7
|
|
1023
|
+
);
|
|
1024
|
+
const tier = score === 0 ? "newcomer" : score < 30 ? "intermediate" : "power_user";
|
|
1025
|
+
return {
|
|
1026
|
+
tier,
|
|
1027
|
+
score,
|
|
1028
|
+
breakdown: { hooks, rules, skills, plugins, claude_md: claudeMd, ecosystems }
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/analysis/classifiers/onboarding.ts
|
|
1033
|
+
function classifyOnboarding(_summary, snapshot, _config) {
|
|
1034
|
+
const level = computeExperienceLevel(snapshot);
|
|
1035
|
+
const recommendations = [];
|
|
1036
|
+
let index = 0;
|
|
1037
|
+
if (level.tier === "newcomer") {
|
|
1038
|
+
if (level.breakdown.hooks === 0) {
|
|
1039
|
+
recommendations.push({
|
|
1040
|
+
id: `rec-onboarding-${index}`,
|
|
1041
|
+
target: "HOOK",
|
|
1042
|
+
confidence: "MEDIUM",
|
|
1043
|
+
pattern_type: "onboarding_start_hooks",
|
|
1044
|
+
title: "Start automating: create your first hook",
|
|
1045
|
+
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.",
|
|
1046
|
+
evidence: {
|
|
1047
|
+
count: 0,
|
|
1048
|
+
examples: ["No hooks detected in your environment"]
|
|
1049
|
+
},
|
|
1050
|
+
suggested_action: "Add a hook in .claude/settings.json hooks section for automation."
|
|
1051
|
+
});
|
|
1052
|
+
index++;
|
|
1053
|
+
}
|
|
1054
|
+
if (level.breakdown.rules === 0) {
|
|
1055
|
+
recommendations.push({
|
|
1056
|
+
id: `rec-onboarding-${index}`,
|
|
1057
|
+
target: "RULE",
|
|
1058
|
+
confidence: "MEDIUM",
|
|
1059
|
+
pattern_type: "onboarding_start_rules",
|
|
1060
|
+
title: "Define coding preferences: add your first rule",
|
|
1061
|
+
description: "Rules (.claude/rules/) codify conventions that Claude follows automatically. They persist across sessions and ensure consistent behavior.",
|
|
1062
|
+
evidence: {
|
|
1063
|
+
count: 0,
|
|
1064
|
+
examples: ["No rules detected in your environment"]
|
|
1065
|
+
},
|
|
1066
|
+
suggested_action: "Create .claude/rules/ directory with a rule for your preferred coding style."
|
|
1067
|
+
});
|
|
1068
|
+
index++;
|
|
1069
|
+
}
|
|
1070
|
+
if (level.breakdown.claude_md === 0) {
|
|
1071
|
+
recommendations.push({
|
|
1072
|
+
id: `rec-onboarding-${index}`,
|
|
1073
|
+
target: "CLAUDE_MD",
|
|
1074
|
+
confidence: "MEDIUM",
|
|
1075
|
+
pattern_type: "onboarding_start_claudemd",
|
|
1076
|
+
title: "Set project context: create CLAUDE.md",
|
|
1077
|
+
description: "CLAUDE.md gives Claude project-specific context \u2014 tech stack, conventions, and constraints. It is loaded automatically at the start of every conversation.",
|
|
1078
|
+
evidence: {
|
|
1079
|
+
count: 0,
|
|
1080
|
+
examples: ["No CLAUDE.md files detected in your environment"]
|
|
1081
|
+
},
|
|
1082
|
+
suggested_action: "Create CLAUDE.md in your project root with project description and conventions."
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
} else if (level.tier === "power_user") {
|
|
1086
|
+
recommendations.push({
|
|
1087
|
+
id: "rec-onboarding-3",
|
|
1088
|
+
target: "SETTINGS",
|
|
1089
|
+
confidence: "LOW",
|
|
1090
|
+
pattern_type: "onboarding_optimize",
|
|
1091
|
+
title: "Consider mechanizing recurring patterns",
|
|
1092
|
+
description: "Your extensive configuration suggests active automation investment. Review for redundancy or upgrade opportunities \u2014 hooks and rules with overlapping concerns can be consolidated.",
|
|
1093
|
+
evidence: {
|
|
1094
|
+
count: level.score,
|
|
1095
|
+
examples: [
|
|
1096
|
+
`${level.breakdown.hooks} hooks, ${level.breakdown.rules} rules, ${level.breakdown.plugins} plugins installed`
|
|
1097
|
+
]
|
|
1098
|
+
},
|
|
1099
|
+
suggested_action: "Review your hooks and rules for overlapping concerns that could be consolidated."
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
return recommendations;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/analysis/classifiers/index.ts
|
|
1106
|
+
var classifiers = [
|
|
1107
|
+
classifyRepeatedPrompts,
|
|
1108
|
+
classifyLongPrompts,
|
|
1109
|
+
classifyPermissionPatterns,
|
|
1110
|
+
classifyCodeCorrections,
|
|
1111
|
+
classifyPersonalInfo,
|
|
1112
|
+
classifyConfigDrift,
|
|
1113
|
+
classifyEcosystemAdaptations,
|
|
1114
|
+
classifyOnboarding
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
// src/schemas/recommendation.ts
|
|
1118
|
+
import { z as z6 } from "zod/v4";
|
|
1119
|
+
var routingTargetSchema = z6.enum([
|
|
1120
|
+
"HOOK",
|
|
1121
|
+
"SKILL",
|
|
1122
|
+
"RULE",
|
|
1123
|
+
"CLAUDE_MD",
|
|
1124
|
+
"MEMORY",
|
|
1125
|
+
"SETTINGS"
|
|
1126
|
+
]);
|
|
1127
|
+
var confidenceSchema = z6.enum(["HIGH", "MEDIUM", "LOW"]);
|
|
1128
|
+
var patternTypeSchema = z6.enum([
|
|
1129
|
+
"repeated_prompt",
|
|
1130
|
+
"long_prompt",
|
|
1131
|
+
"permission-always-approved",
|
|
1132
|
+
"code_correction",
|
|
1133
|
+
"personal_info",
|
|
1134
|
+
"config_drift",
|
|
1135
|
+
"version_update",
|
|
1136
|
+
"ecosystem_gsd",
|
|
1137
|
+
"ecosystem_cog",
|
|
1138
|
+
"onboarding_start_hooks",
|
|
1139
|
+
"onboarding_start_rules",
|
|
1140
|
+
"onboarding_start_claudemd",
|
|
1141
|
+
"onboarding_optimize",
|
|
1142
|
+
"scan_redundancy",
|
|
1143
|
+
"scan_missing_mechanization",
|
|
1144
|
+
"scan_stale_reference"
|
|
1145
|
+
]);
|
|
1146
|
+
var recommendationSchema = z6.object({
|
|
1147
|
+
id: z6.string(),
|
|
1148
|
+
target: routingTargetSchema,
|
|
1149
|
+
confidence: confidenceSchema,
|
|
1150
|
+
pattern_type: patternTypeSchema,
|
|
1151
|
+
title: z6.string(),
|
|
1152
|
+
description: z6.string(),
|
|
1153
|
+
evidence: z6.object({
|
|
1154
|
+
count: z6.number(),
|
|
1155
|
+
sessions: z6.number().optional(),
|
|
1156
|
+
examples: z6.array(z6.string()).max(3)
|
|
1157
|
+
}),
|
|
1158
|
+
suggested_action: z6.string(),
|
|
1159
|
+
ecosystem_context: z6.string().optional()
|
|
1160
|
+
});
|
|
1161
|
+
var DEFAULT_THRESHOLDS = {
|
|
1162
|
+
repeated_prompt_min_count: 5,
|
|
1163
|
+
repeated_prompt_high_count: 10,
|
|
1164
|
+
repeated_prompt_high_sessions: 3,
|
|
1165
|
+
repeated_prompt_medium_sessions: 2,
|
|
1166
|
+
long_prompt_min_words: 200,
|
|
1167
|
+
long_prompt_min_count: 2,
|
|
1168
|
+
long_prompt_high_words: 300,
|
|
1169
|
+
long_prompt_high_count: 3,
|
|
1170
|
+
permission_approval_min_count: 10,
|
|
1171
|
+
permission_approval_min_sessions: 3,
|
|
1172
|
+
permission_approval_high_count: 15,
|
|
1173
|
+
permission_approval_high_sessions: 4,
|
|
1174
|
+
code_correction_min_failure_rate: 0.3,
|
|
1175
|
+
code_correction_min_failures: 3
|
|
1176
|
+
};
|
|
1177
|
+
var analysisConfigSchema = z6.object({
|
|
1178
|
+
thresholds: z6.object({
|
|
1179
|
+
repeated_prompt_min_count: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_min_count),
|
|
1180
|
+
repeated_prompt_high_count: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_count),
|
|
1181
|
+
repeated_prompt_high_sessions: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_high_sessions),
|
|
1182
|
+
repeated_prompt_medium_sessions: z6.number().default(DEFAULT_THRESHOLDS.repeated_prompt_medium_sessions),
|
|
1183
|
+
long_prompt_min_words: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_min_words),
|
|
1184
|
+
long_prompt_min_count: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_min_count),
|
|
1185
|
+
long_prompt_high_words: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_high_words),
|
|
1186
|
+
long_prompt_high_count: z6.number().default(DEFAULT_THRESHOLDS.long_prompt_high_count),
|
|
1187
|
+
permission_approval_min_count: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_min_count),
|
|
1188
|
+
permission_approval_min_sessions: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_min_sessions),
|
|
1189
|
+
permission_approval_high_count: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_high_count),
|
|
1190
|
+
permission_approval_high_sessions: z6.number().default(DEFAULT_THRESHOLDS.permission_approval_high_sessions),
|
|
1191
|
+
code_correction_min_failure_rate: z6.number().default(DEFAULT_THRESHOLDS.code_correction_min_failure_rate),
|
|
1192
|
+
code_correction_min_failures: z6.number().default(DEFAULT_THRESHOLDS.code_correction_min_failures)
|
|
1193
|
+
}).default(() => ({ ...DEFAULT_THRESHOLDS })),
|
|
1194
|
+
max_recommendations: z6.number().default(20)
|
|
1195
|
+
}).default(() => ({
|
|
1196
|
+
thresholds: { ...DEFAULT_THRESHOLDS },
|
|
1197
|
+
max_recommendations: 20
|
|
1198
|
+
}));
|
|
1199
|
+
var analysisResultSchema = z6.object({
|
|
1200
|
+
generated_at: z6.iso.datetime(),
|
|
1201
|
+
summary_period: z6.object({
|
|
1202
|
+
since: z6.string(),
|
|
1203
|
+
until: z6.string(),
|
|
1204
|
+
days: z6.number()
|
|
1205
|
+
}),
|
|
1206
|
+
recommendations: z6.array(recommendationSchema),
|
|
1207
|
+
metadata: z6.object({
|
|
1208
|
+
classifier_count: z6.number(),
|
|
1209
|
+
patterns_evaluated: z6.number(),
|
|
1210
|
+
environment_ecosystems: z6.array(z6.string()),
|
|
1211
|
+
claude_code_version: z6.string()
|
|
1212
|
+
})
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// src/analysis/analyzer.ts
|
|
1216
|
+
var CONFIDENCE_ORDER = {
|
|
1217
|
+
HIGH: 0,
|
|
1218
|
+
MEDIUM: 1,
|
|
1219
|
+
LOW: 2
|
|
1220
|
+
};
|
|
1221
|
+
function sortRecommendations(a, b) {
|
|
1222
|
+
const confDiff = (CONFIDENCE_ORDER[a.confidence] ?? 3) - (CONFIDENCE_ORDER[b.confidence] ?? 3);
|
|
1223
|
+
if (confDiff !== 0) return confDiff;
|
|
1224
|
+
return b.evidence.count - a.evidence.count;
|
|
1225
|
+
}
|
|
1226
|
+
function adjustConfidence(recommendations, summaries) {
|
|
1227
|
+
const rateByType = new Map(
|
|
1228
|
+
summaries.map((s) => [s.pattern_type, s.persistence_rate])
|
|
1229
|
+
);
|
|
1230
|
+
return recommendations.map((rec) => {
|
|
1231
|
+
const rate = rateByType.get(rec.pattern_type);
|
|
1232
|
+
if (rate === void 0 || rate >= 0.7) return rec;
|
|
1233
|
+
const downgraded = {
|
|
1234
|
+
HIGH: "MEDIUM",
|
|
1235
|
+
MEDIUM: "LOW",
|
|
1236
|
+
LOW: "LOW"
|
|
1237
|
+
};
|
|
1238
|
+
return {
|
|
1239
|
+
...rec,
|
|
1240
|
+
confidence: downgraded[rec.confidence] ?? rec.confidence
|
|
1241
|
+
};
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
function analyze(summary, snapshot, config, outcomeSummaries) {
|
|
1245
|
+
const mergedConfig = config ?? analysisConfigSchema.parse({});
|
|
1246
|
+
const recommendations = [];
|
|
1247
|
+
for (const classify of classifiers) {
|
|
1248
|
+
const results = classify(summary, snapshot, mergedConfig);
|
|
1249
|
+
recommendations.push(...results);
|
|
1250
|
+
}
|
|
1251
|
+
const adjusted = outcomeSummaries && outcomeSummaries.length > 0 ? adjustConfidence(recommendations, outcomeSummaries) : recommendations;
|
|
1252
|
+
adjusted.sort(sortRecommendations);
|
|
1253
|
+
const capped = adjusted.slice(0, mergedConfig.max_recommendations);
|
|
1254
|
+
const patternsEvaluated = summary.top_repeated_prompts.length + summary.long_prompts.length + summary.permission_patterns.length + summary.tool_frequency.length;
|
|
1255
|
+
return analysisResultSchema.parse({
|
|
1256
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1257
|
+
summary_period: summary.period,
|
|
1258
|
+
recommendations: capped,
|
|
1259
|
+
metadata: {
|
|
1260
|
+
classifier_count: classifiers.length,
|
|
1261
|
+
patterns_evaluated: patternsEvaluated,
|
|
1262
|
+
environment_ecosystems: snapshot.detected_ecosystems,
|
|
1263
|
+
claude_code_version: snapshot.claude_code.version
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/analysis/outcome-tracker.ts
|
|
1269
|
+
import { readFile as readFile5, appendFile } from "fs/promises";
|
|
1270
|
+
|
|
1271
|
+
// src/delivery/state.ts
|
|
1272
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1273
|
+
import writeFileAtomic5 from "write-file-atomic";
|
|
1274
|
+
|
|
1275
|
+
// src/schemas/delivery.ts
|
|
1276
|
+
import { z as z7 } from "zod/v4";
|
|
1277
|
+
var recommendationStatusSchema = z7.enum(["pending", "applied", "dismissed"]);
|
|
1278
|
+
var recommendationStateEntrySchema = z7.object({
|
|
1279
|
+
id: z7.string(),
|
|
1280
|
+
status: recommendationStatusSchema,
|
|
1281
|
+
updated_at: z7.iso.datetime(),
|
|
1282
|
+
applied_details: z7.string().optional()
|
|
1283
|
+
});
|
|
1284
|
+
var recommendationStateSchema = z7.object({
|
|
1285
|
+
entries: z7.array(recommendationStateEntrySchema),
|
|
1286
|
+
last_updated: z7.iso.datetime()
|
|
1287
|
+
});
|
|
1288
|
+
var autoApplyLogEntrySchema = z7.object({
|
|
1289
|
+
timestamp: z7.iso.datetime(),
|
|
1290
|
+
recommendation_id: z7.string(),
|
|
1291
|
+
target: z7.string(),
|
|
1292
|
+
action: z7.string(),
|
|
1293
|
+
success: z7.boolean(),
|
|
1294
|
+
details: z7.string().optional(),
|
|
1295
|
+
backup_path: z7.string().optional()
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// src/delivery/state.ts
|
|
1299
|
+
async function loadState() {
|
|
1300
|
+
try {
|
|
1301
|
+
const raw = await readFile4(paths.recommendationState, "utf-8");
|
|
1302
|
+
return recommendationStateSchema.parse(JSON.parse(raw));
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
1305
|
+
return { entries: [], last_updated: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1306
|
+
}
|
|
1307
|
+
throw err;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function isNodeError(err) {
|
|
1311
|
+
return err instanceof Error && "code" in err;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/schemas/onboarding.ts
|
|
1315
|
+
import { z as z8 } from "zod/v4";
|
|
1316
|
+
var experienceTierSchema = z8.enum(["newcomer", "intermediate", "power_user"]);
|
|
1317
|
+
var experienceLevelSchema = z8.object({
|
|
1318
|
+
tier: experienceTierSchema,
|
|
1319
|
+
score: z8.number().min(0).max(100),
|
|
1320
|
+
breakdown: z8.object({
|
|
1321
|
+
hooks: z8.number(),
|
|
1322
|
+
rules: z8.number(),
|
|
1323
|
+
skills: z8.number(),
|
|
1324
|
+
plugins: z8.number(),
|
|
1325
|
+
claude_md: z8.number(),
|
|
1326
|
+
ecosystems: z8.number()
|
|
1327
|
+
})
|
|
1328
|
+
});
|
|
1329
|
+
var outcomeEntrySchema = z8.object({
|
|
1330
|
+
recommendation_id: z8.string(),
|
|
1331
|
+
pattern_type: z8.string(),
|
|
1332
|
+
target: z8.string(),
|
|
1333
|
+
applied_at: z8.iso.datetime(),
|
|
1334
|
+
checked_at: z8.iso.datetime(),
|
|
1335
|
+
persisted: z8.boolean(),
|
|
1336
|
+
checks_since_applied: z8.number(),
|
|
1337
|
+
outcome: z8.enum(["positive", "negative", "monitoring"])
|
|
1338
|
+
});
|
|
1339
|
+
var outcomeSummarySchema = z8.object({
|
|
1340
|
+
pattern_type: z8.string(),
|
|
1341
|
+
total_applied: z8.number(),
|
|
1342
|
+
total_persisted: z8.number(),
|
|
1343
|
+
total_reverted: z8.number(),
|
|
1344
|
+
persistence_rate: z8.number()
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// src/analysis/outcome-tracker.ts
|
|
1348
|
+
async function trackOutcomes(snapshot) {
|
|
1349
|
+
const state = await loadState();
|
|
1350
|
+
const applied = state.entries.filter((e) => e.status === "applied");
|
|
1351
|
+
if (applied.length === 0) return [];
|
|
1352
|
+
const history = await loadOutcomeHistory();
|
|
1353
|
+
const results = [];
|
|
1354
|
+
for (const entry of applied) {
|
|
1355
|
+
const priorEntries = history.filter(
|
|
1356
|
+
(h) => h.recommendation_id === entry.id
|
|
1357
|
+
);
|
|
1358
|
+
const latest = priorEntries.length > 0 ? priorEntries[priorEntries.length - 1] : void 0;
|
|
1359
|
+
const checksCount = latest ? latest.checks_since_applied + 1 : 1;
|
|
1360
|
+
const persisted = checkPersistence(entry, snapshot);
|
|
1361
|
+
let outcome;
|
|
1362
|
+
if (!persisted) {
|
|
1363
|
+
outcome = "negative";
|
|
1364
|
+
} else if (checksCount >= 5) {
|
|
1365
|
+
outcome = "positive";
|
|
1366
|
+
} else {
|
|
1367
|
+
outcome = "monitoring";
|
|
1368
|
+
}
|
|
1369
|
+
const patternType = inferPatternType(entry.id);
|
|
1370
|
+
const target = inferTarget(entry.id);
|
|
1371
|
+
const outcomeEntry = {
|
|
1372
|
+
recommendation_id: entry.id,
|
|
1373
|
+
pattern_type: patternType,
|
|
1374
|
+
target,
|
|
1375
|
+
applied_at: entry.updated_at,
|
|
1376
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1377
|
+
persisted,
|
|
1378
|
+
checks_since_applied: checksCount,
|
|
1379
|
+
outcome
|
|
1380
|
+
};
|
|
1381
|
+
results.push(outcomeEntry);
|
|
1382
|
+
await appendOutcome(outcomeEntry);
|
|
1383
|
+
}
|
|
1384
|
+
return results;
|
|
1385
|
+
}
|
|
1386
|
+
function checkPersistence(entry, snapshot) {
|
|
1387
|
+
if (entry.applied_details) {
|
|
1388
|
+
const toolMatch = entry.applied_details.match(/Added (\w+) to allowedTools/);
|
|
1389
|
+
if (toolMatch) {
|
|
1390
|
+
const toolName = toolMatch[1];
|
|
1391
|
+
const userSettings = snapshot.settings.user;
|
|
1392
|
+
if (!userSettings) return false;
|
|
1393
|
+
const allowedTools = userSettings.allowedTools;
|
|
1394
|
+
if (!Array.isArray(allowedTools)) return false;
|
|
1395
|
+
return allowedTools.includes(toolName);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (entry.id.startsWith("rec-repeated-")) {
|
|
1399
|
+
return snapshot.installed_tools.hooks.length > 0;
|
|
1400
|
+
}
|
|
1401
|
+
if (entry.id.startsWith("rec-long-")) {
|
|
1402
|
+
return snapshot.installed_tools.skills.length > 0;
|
|
1403
|
+
}
|
|
1404
|
+
if (entry.id.startsWith("rec-correction-")) {
|
|
1405
|
+
return snapshot.installed_tools.rules.length > 0;
|
|
1406
|
+
}
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
async function appendOutcome(entry) {
|
|
1410
|
+
await appendFile(
|
|
1411
|
+
paths.outcomeHistory,
|
|
1412
|
+
JSON.stringify(entry) + "\n",
|
|
1413
|
+
"utf-8"
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
async function loadOutcomeHistory() {
|
|
1417
|
+
let raw;
|
|
1418
|
+
try {
|
|
1419
|
+
raw = await readFile5(paths.outcomeHistory, "utf-8");
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (isNodeError2(err) && err.code === "ENOENT") {
|
|
1422
|
+
return [];
|
|
1423
|
+
}
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
const entries = [];
|
|
1427
|
+
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
1428
|
+
for (const line of lines) {
|
|
1429
|
+
try {
|
|
1430
|
+
const parsed = JSON.parse(line);
|
|
1431
|
+
const result = outcomeEntrySchema.safeParse(parsed);
|
|
1432
|
+
if (result.success) {
|
|
1433
|
+
entries.push(result.data);
|
|
1434
|
+
}
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return entries;
|
|
1439
|
+
}
|
|
1440
|
+
function computeOutcomeSummaries(history) {
|
|
1441
|
+
if (history.length === 0) return [];
|
|
1442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1443
|
+
for (const entry of history) {
|
|
1444
|
+
const group = groups.get(entry.pattern_type) ?? [];
|
|
1445
|
+
group.push(entry);
|
|
1446
|
+
groups.set(entry.pattern_type, group);
|
|
1447
|
+
}
|
|
1448
|
+
const summaries = [];
|
|
1449
|
+
for (const [patternType, entries] of groups) {
|
|
1450
|
+
const latestByRec = /* @__PURE__ */ new Map();
|
|
1451
|
+
for (const entry of entries) {
|
|
1452
|
+
latestByRec.set(entry.recommendation_id, entry);
|
|
1453
|
+
}
|
|
1454
|
+
let totalPersisted = 0;
|
|
1455
|
+
let totalReverted = 0;
|
|
1456
|
+
for (const entry of latestByRec.values()) {
|
|
1457
|
+
if (entry.outcome === "positive") {
|
|
1458
|
+
totalPersisted++;
|
|
1459
|
+
} else if (entry.outcome === "negative") {
|
|
1460
|
+
totalReverted++;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const totalApplied = latestByRec.size;
|
|
1464
|
+
const persistenceRate = totalApplied > 0 ? totalPersisted / totalApplied : 0;
|
|
1465
|
+
summaries.push({
|
|
1466
|
+
pattern_type: patternType,
|
|
1467
|
+
total_applied: totalApplied,
|
|
1468
|
+
total_persisted: totalPersisted,
|
|
1469
|
+
total_reverted: totalReverted,
|
|
1470
|
+
persistence_rate: persistenceRate
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
return summaries;
|
|
1474
|
+
}
|
|
1475
|
+
function inferPatternType(id) {
|
|
1476
|
+
if (id.startsWith("rec-repeated-")) return "repeated_prompt";
|
|
1477
|
+
if (id.startsWith("rec-long-")) return "long_prompt";
|
|
1478
|
+
if (id.startsWith("rec-permission-always-approved-")) return "permission-always-approved";
|
|
1479
|
+
if (id.startsWith("rec-correction-")) return "code_correction";
|
|
1480
|
+
if (id.startsWith("rec-personal-")) return "personal_info";
|
|
1481
|
+
if (id.startsWith("rec-drift-")) return "config_drift";
|
|
1482
|
+
if (id.startsWith("rec-ecosystem-")) return "version_update";
|
|
1483
|
+
if (id.startsWith("rec-onboarding-")) return "onboarding_start_hooks";
|
|
1484
|
+
if (id.startsWith("rec-tool-preference-")) return "tool-preference";
|
|
1485
|
+
return "unknown";
|
|
1486
|
+
}
|
|
1487
|
+
function inferTarget(id) {
|
|
1488
|
+
if (id.startsWith("rec-repeated-")) return "HOOK";
|
|
1489
|
+
if (id.startsWith("rec-long-")) return "SKILL";
|
|
1490
|
+
if (id.startsWith("rec-permission-always-approved-")) return "SETTINGS";
|
|
1491
|
+
if (id.startsWith("rec-correction-")) return "RULE";
|
|
1492
|
+
if (id.startsWith("rec-personal-")) return "MEMORY";
|
|
1493
|
+
if (id.startsWith("rec-drift-")) return "CLAUDE_MD";
|
|
1494
|
+
if (id.startsWith("rec-ecosystem-")) return "CLAUDE_MD";
|
|
1495
|
+
if (id.startsWith("rec-tool-preference-")) return "SETTINGS";
|
|
1496
|
+
if (id.startsWith("rec-onboarding-")) return "HOOK";
|
|
1497
|
+
return "MEMORY";
|
|
1498
|
+
}
|
|
1499
|
+
function isNodeError2(err) {
|
|
1500
|
+
return err instanceof Error && "code" in err;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/analysis/trigger.ts
|
|
1504
|
+
import writeFileAtomic6 from "write-file-atomic";
|
|
1505
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1506
|
+
import { lock as lock2 } from "proper-lockfile";
|
|
1507
|
+
var COOLDOWN_MS = 6e4;
|
|
1508
|
+
async function writeAnalysisResult(result) {
|
|
1509
|
+
await ensureInit();
|
|
1510
|
+
await writeFileAtomic6(paths.analysisResult, JSON.stringify(result, null, 2));
|
|
1511
|
+
}
|
|
1512
|
+
async function runAnalysis(cwd) {
|
|
1513
|
+
const summary = await preProcess();
|
|
1514
|
+
const snapshot = await scanEnvironment(cwd);
|
|
1515
|
+
let outcomeSummaries;
|
|
1516
|
+
try {
|
|
1517
|
+
await trackOutcomes(snapshot);
|
|
1518
|
+
const history = await loadOutcomeHistory();
|
|
1519
|
+
outcomeSummaries = computeOutcomeSummaries(history);
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
const result = analyze(summary, snapshot, void 0, outcomeSummaries);
|
|
1523
|
+
await writeAnalysisResult(result);
|
|
1524
|
+
return result;
|
|
1525
|
+
}
|
|
1526
|
+
async function resetCounterWithTimestamp() {
|
|
1527
|
+
try {
|
|
1528
|
+
await readFile6(paths.counter, "utf-8");
|
|
1529
|
+
} catch {
|
|
1530
|
+
const initial = {
|
|
1531
|
+
total: 0,
|
|
1532
|
+
session: {},
|
|
1533
|
+
last_updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1534
|
+
};
|
|
1535
|
+
await writeFileAtomic6(paths.counter, JSON.stringify(initial, null, 2));
|
|
1536
|
+
}
|
|
1537
|
+
const release = await lock2(paths.counter, {
|
|
1538
|
+
retries: { retries: 50, minTimeout: 20, maxTimeout: 1e3, randomize: true },
|
|
1539
|
+
stale: 1e4
|
|
1540
|
+
});
|
|
1541
|
+
try {
|
|
1542
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1543
|
+
const data = {
|
|
1544
|
+
total: 0,
|
|
1545
|
+
session: {},
|
|
1546
|
+
last_analysis: now,
|
|
1547
|
+
last_updated: now
|
|
1548
|
+
};
|
|
1549
|
+
await writeFileAtomic6(paths.counter, JSON.stringify(data, null, 2));
|
|
1550
|
+
} finally {
|
|
1551
|
+
await release();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function checkAndTriggerAnalysis(cwd) {
|
|
1555
|
+
const config = await loadConfig();
|
|
1556
|
+
if (!config.analysis.enabled) return false;
|
|
1557
|
+
const counter = await readCounter();
|
|
1558
|
+
if (counter.total < config.analysis.threshold) return false;
|
|
1559
|
+
if (counter.last_analysis) {
|
|
1560
|
+
const elapsed = Date.now() - new Date(counter.last_analysis).getTime();
|
|
1561
|
+
if (elapsed < COOLDOWN_MS) return false;
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
await runAnalysis(cwd);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
await resetCounterWithTimestamp();
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/hooks/shared.ts
|
|
1573
|
+
function readFromStream(stream) {
|
|
1574
|
+
return new Promise((resolve, reject) => {
|
|
1575
|
+
let data = "";
|
|
1576
|
+
stream.setEncoding("utf-8");
|
|
1577
|
+
stream.on("data", (chunk) => {
|
|
1578
|
+
data += chunk;
|
|
1579
|
+
});
|
|
1580
|
+
stream.on("end", () => resolve(data));
|
|
1581
|
+
stream.on("error", reject);
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
function readStdin() {
|
|
1585
|
+
return readFromStream(process.stdin);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/hooks/stop.ts
|
|
1589
|
+
async function handleStop(rawJson) {
|
|
1590
|
+
try {
|
|
1591
|
+
const input = stopInputSchema.parse(JSON.parse(rawJson));
|
|
1592
|
+
if (input.stop_hook_active) return;
|
|
1593
|
+
await checkAndTriggerAnalysis(input.cwd);
|
|
1594
|
+
} catch {
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async function main() {
|
|
1598
|
+
try {
|
|
1599
|
+
const raw = await readStdin();
|
|
1600
|
+
await handleStop(raw);
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
process.exit(0);
|
|
1604
|
+
}
|
|
1605
|
+
main();
|
|
1606
|
+
export {
|
|
1607
|
+
handleStop
|
|
1608
|
+
};
|
|
1609
|
+
//# sourceMappingURL=stop.js.map
|