@miphamai/cli 0.32.6 → 0.33.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/package.json +1 -1
- package/src/core/auto-corrector.ts +220 -0
- package/src/core/auto-memory.ts +29 -0
- package/src/core/engine.ts +97 -1
- package/src/core/error-signature-db.ts +324 -0
- package/src/core/immune-memory-gc.ts +187 -0
- package/src/core/instructions.ts +14 -0
- package/src/core/meta-rule-engine.ts +602 -0
- package/src/core/preflight-checker.ts +218 -0
- package/src/tools/file/grep.ts +9 -1
- package/src/ui/command-picker.tsx +34 -1
- package/src/ui/commands.ts +268 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIS (Self-Immune System) Phase 0: Error Signature Database
|
|
3
|
+
*
|
|
4
|
+
* Persistent storage for known error patterns. Each signature captures:
|
|
5
|
+
* - What error occurred (pattern matching against command/error text)
|
|
6
|
+
* - How to fix it (strategy + action)
|
|
7
|
+
* - How reliable the fix is (success rate tracking)
|
|
8
|
+
*
|
|
9
|
+
* Storage: JSON file at ~/.mipham/sis/error-signatures.json
|
|
10
|
+
* Pattern: follows EffectivenessTracker's file-based persistence model
|
|
11
|
+
*
|
|
12
|
+
* Integration:
|
|
13
|
+
* AutoMemoryEngine.feedCrsiPipeline() → ErrorSignatureDB.insert()
|
|
14
|
+
* PreFlightChecker.check() → ErrorSignatureDB.match()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { randomUUID } from 'node:crypto'
|
|
21
|
+
|
|
22
|
+
// ── Types ──
|
|
23
|
+
|
|
24
|
+
export interface ErrorSignature {
|
|
25
|
+
/** Unique identifier */
|
|
26
|
+
id: string
|
|
27
|
+
/** Substring or regex pattern to match against command/error text */
|
|
28
|
+
pattern: string
|
|
29
|
+
/** Failure category: timeout | tool-params | import | search | semantic */
|
|
30
|
+
category: string
|
|
31
|
+
/** Associated tool name (e.g. 'Bash', 'Write') */
|
|
32
|
+
toolName: string
|
|
33
|
+
/** How to fix: replace params, prepend to command, append, warn only, or block entirely */
|
|
34
|
+
fixStrategy: 'replace' | 'prepend' | 'append' | 'warn' | 'block'
|
|
35
|
+
/** The fix action (e.g. 'pnpm install' replaces 'npm install') */
|
|
36
|
+
fixAction: string
|
|
37
|
+
/** Human-readable explanation of the fix */
|
|
38
|
+
explanation: string
|
|
39
|
+
/** Number of times this error has been encountered */
|
|
40
|
+
occurrences: number
|
|
41
|
+
/** Number of times the fix was applied successfully */
|
|
42
|
+
successCount: number
|
|
43
|
+
/** ISO timestamp of first encounter */
|
|
44
|
+
firstSeen: string
|
|
45
|
+
/** ISO timestamp of most recent encounter */
|
|
46
|
+
lastSeen: string
|
|
47
|
+
/** Fix success rate (0.0-1.0) */
|
|
48
|
+
successRate: number
|
|
49
|
+
/** Current status */
|
|
50
|
+
status: 'active' | 'degraded' | 'retired'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ErrorSignatureStats {
|
|
54
|
+
total: number
|
|
55
|
+
active: number
|
|
56
|
+
degraded: number
|
|
57
|
+
retired: number
|
|
58
|
+
avgSuccessRate: number
|
|
59
|
+
totalInterceptions: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Constants ──
|
|
63
|
+
|
|
64
|
+
const DEFAULT_STORE_DIR = join(homedir(), '.mipham', 'sis')
|
|
65
|
+
const STORE_FILE = 'error-signatures.json'
|
|
66
|
+
const MIN_SUCCESS_RATE = 0.5 // below this → degraded
|
|
67
|
+
const RETIREMENT_RATE = 0.2 // below this and > 90 days old → retired
|
|
68
|
+
const RETENTION_DAYS = 90 // auto-clean retired signatures older than this
|
|
69
|
+
|
|
70
|
+
// ── Database ──
|
|
71
|
+
|
|
72
|
+
export class ErrorSignatureDB {
|
|
73
|
+
private signatures: Map<string, ErrorSignature> = new Map()
|
|
74
|
+
private storePath: string
|
|
75
|
+
private dirty = false
|
|
76
|
+
|
|
77
|
+
constructor(storeDir: string = DEFAULT_STORE_DIR) {
|
|
78
|
+
this.storePath = join(storeDir, STORE_FILE)
|
|
79
|
+
this.load()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Persistence ──
|
|
83
|
+
|
|
84
|
+
/** Load signatures from disk. Non-existent file = clean slate. */
|
|
85
|
+
private load(): void {
|
|
86
|
+
try {
|
|
87
|
+
if (!existsSync(this.storePath)) return
|
|
88
|
+
const raw = readFileSync(this.storePath, 'utf-8')
|
|
89
|
+
const arr: ErrorSignature[] = JSON.parse(raw)
|
|
90
|
+
for (const sig of arr) {
|
|
91
|
+
this.signatures.set(sig.id, sig)
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Corrupted or missing file — start fresh
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Persist signatures to disk. Debounced: only writes if dirty. */
|
|
99
|
+
private save(): void {
|
|
100
|
+
if (!this.dirty) return
|
|
101
|
+
try {
|
|
102
|
+
mkdirSync(join(this.storePath, '..'), { recursive: true })
|
|
103
|
+
const arr = Array.from(this.signatures.values())
|
|
104
|
+
writeFileSync(this.storePath, JSON.stringify(arr, null, 2), 'utf-8')
|
|
105
|
+
this.dirty = false
|
|
106
|
+
} catch {
|
|
107
|
+
// Best-effort persistence — never crash on write failure
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── CRUD ──
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Insert a new error signature. If a signature with the same pattern +
|
|
115
|
+
* toolName + category already exists, increment its occurrence count instead.
|
|
116
|
+
*/
|
|
117
|
+
insert(
|
|
118
|
+
sig: Omit<
|
|
119
|
+
ErrorSignature,
|
|
120
|
+
'id' | 'occurrences' | 'successCount' | 'successRate' | 'firstSeen' | 'lastSeen' | 'status'
|
|
121
|
+
>,
|
|
122
|
+
): ErrorSignature {
|
|
123
|
+
// Dedup: same pattern + toolName + category → update existing
|
|
124
|
+
const existing = this.findByPattern(sig.pattern, sig.toolName, sig.category)
|
|
125
|
+
if (existing) {
|
|
126
|
+
existing.occurrences++
|
|
127
|
+
existing.lastSeen = new Date().toISOString()
|
|
128
|
+
existing.successRate =
|
|
129
|
+
existing.occurrences > 0 ? existing.successCount / existing.occurrences : 1.0
|
|
130
|
+
this.dirty = true
|
|
131
|
+
this.save()
|
|
132
|
+
return existing
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const now = new Date().toISOString()
|
|
136
|
+
const id = `sis-${randomUUID().slice(0, 8)}`
|
|
137
|
+
const full: ErrorSignature = {
|
|
138
|
+
...sig,
|
|
139
|
+
id,
|
|
140
|
+
occurrences: 1,
|
|
141
|
+
successCount: 0,
|
|
142
|
+
successRate: 0, // starts at 0 — must prove itself before auto-apply
|
|
143
|
+
firstSeen: now,
|
|
144
|
+
lastSeen: now,
|
|
145
|
+
status: 'active',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.signatures.set(id, full)
|
|
149
|
+
this.dirty = true
|
|
150
|
+
this.save()
|
|
151
|
+
return full
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Match a tool call against known error signatures.
|
|
156
|
+
* Returns the best-matching active signature, or null if no match.
|
|
157
|
+
*
|
|
158
|
+
* Matching strategy:
|
|
159
|
+
* 1. Exact toolName match
|
|
160
|
+
* 2. Pattern substring match in command/error text
|
|
161
|
+
* 3. Category match as tiebreaker
|
|
162
|
+
*/
|
|
163
|
+
match(toolName: string, params: Record<string, unknown>): ErrorSignature | null {
|
|
164
|
+
const text = this.paramsToText(toolName, params)
|
|
165
|
+
let best: ErrorSignature | null = null
|
|
166
|
+
let bestScore = 0
|
|
167
|
+
|
|
168
|
+
for (const sig of this.signatures.values()) {
|
|
169
|
+
if (sig.status === 'retired') continue
|
|
170
|
+
if (sig.toolName !== toolName) continue
|
|
171
|
+
|
|
172
|
+
// Pattern must match — this is the primary filter
|
|
173
|
+
if (!text.toLowerCase().includes(sig.pattern.toLowerCase())) continue
|
|
174
|
+
|
|
175
|
+
// Score tiebreakers when multiple signatures match
|
|
176
|
+
let score = 10 // base score for pattern match
|
|
177
|
+
score += sig.successRate * 5
|
|
178
|
+
score += Math.min(sig.occurrences / 10, 1) * 2
|
|
179
|
+
if (sig.status === 'active') score += 1
|
|
180
|
+
|
|
181
|
+
if (score > bestScore) {
|
|
182
|
+
bestScore = score
|
|
183
|
+
best = sig
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return best
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Record the result of applying a fix for a given signature.
|
|
192
|
+
* Updates success rate and may auto-degrade/retire low-performing signatures.
|
|
193
|
+
*/
|
|
194
|
+
recordResult(id: string, success: boolean): void {
|
|
195
|
+
const sig = this.signatures.get(id)
|
|
196
|
+
if (!sig) return
|
|
197
|
+
|
|
198
|
+
if (success) {
|
|
199
|
+
sig.successCount++
|
|
200
|
+
}
|
|
201
|
+
sig.successRate = sig.occurrences > 0 ? sig.successCount / sig.occurrences : 1.0
|
|
202
|
+
|
|
203
|
+
// Auto-degrade
|
|
204
|
+
if (sig.successRate < MIN_SUCCESS_RATE && sig.status === 'active') {
|
|
205
|
+
sig.status = 'degraded'
|
|
206
|
+
}
|
|
207
|
+
// Auto-retire
|
|
208
|
+
const ageDays = (Date.now() - new Date(sig.firstSeen).getTime()) / (1000 * 60 * 60 * 24)
|
|
209
|
+
if (sig.successRate < RETIREMENT_RATE && ageDays > 90 && sig.status === 'degraded') {
|
|
210
|
+
sig.status = 'retired'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.dirty = true
|
|
214
|
+
this.save()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Query ──
|
|
218
|
+
|
|
219
|
+
/** Get all active and degraded signatures (excludes retired). */
|
|
220
|
+
getActive(): ErrorSignature[] {
|
|
221
|
+
return Array.from(this.signatures.values())
|
|
222
|
+
.filter((s) => s.status !== 'retired')
|
|
223
|
+
.sort((a, b) => b.occurrences - a.occurrences)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Get a specific signature by ID. */
|
|
227
|
+
get(id: string): ErrorSignature | undefined {
|
|
228
|
+
return this.signatures.get(id)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Manually retire a signature. */
|
|
232
|
+
retire(id: string): boolean {
|
|
233
|
+
const sig = this.signatures.get(id)
|
|
234
|
+
if (!sig) return false
|
|
235
|
+
sig.status = 'retired'
|
|
236
|
+
this.dirty = true
|
|
237
|
+
this.save()
|
|
238
|
+
return true
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Get aggregate statistics. */
|
|
242
|
+
getStats(): ErrorSignatureStats {
|
|
243
|
+
const all = Array.from(this.signatures.values())
|
|
244
|
+
const active = all.filter((s) => s.status === 'active')
|
|
245
|
+
const degraded = all.filter((s) => s.status === 'degraded')
|
|
246
|
+
const retired = all.filter((s) => s.status === 'retired')
|
|
247
|
+
|
|
248
|
+
const avgSuccessRate =
|
|
249
|
+
active.length > 0 ? active.reduce((sum, s) => sum + s.successRate, 0) / active.length : 0
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
total: all.length,
|
|
253
|
+
active: active.length,
|
|
254
|
+
degraded: degraded.length,
|
|
255
|
+
retired: retired.length,
|
|
256
|
+
avgSuccessRate: Math.round(avgSuccessRate * 100) / 100,
|
|
257
|
+
totalInterceptions: all.reduce((sum, s) => sum + s.occurrences, 0),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clean up old retired signatures and low-performing degraded ones.
|
|
263
|
+
* Called periodically (e.g. on daemon startup or via /sis cleanup).
|
|
264
|
+
*/
|
|
265
|
+
cleanup(retentionDays: number = RETENTION_DAYS): number {
|
|
266
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000
|
|
267
|
+
let removed = 0
|
|
268
|
+
|
|
269
|
+
for (const [id, sig] of this.signatures) {
|
|
270
|
+
if (sig.status === 'retired' && new Date(sig.lastSeen).getTime() < cutoff) {
|
|
271
|
+
this.signatures.delete(id)
|
|
272
|
+
removed++
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (removed > 0) {
|
|
277
|
+
this.dirty = true
|
|
278
|
+
this.save()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return removed
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Clear all signatures. Used for testing and manual reset. */
|
|
285
|
+
clear(): void {
|
|
286
|
+
this.signatures.clear()
|
|
287
|
+
this.dirty = true
|
|
288
|
+
this.save()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Private Helpers ──
|
|
292
|
+
|
|
293
|
+
/** Find an existing signature by pattern + toolName + category. */
|
|
294
|
+
private findByPattern(
|
|
295
|
+
pattern: string,
|
|
296
|
+
toolName: string,
|
|
297
|
+
category: string,
|
|
298
|
+
): ErrorSignature | undefined {
|
|
299
|
+
for (const sig of this.signatures.values()) {
|
|
300
|
+
if (sig.pattern === pattern && sig.toolName === toolName && sig.category === category) {
|
|
301
|
+
return sig
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Convert tool params to a searchable text blob for pattern matching. */
|
|
308
|
+
private paramsToText(toolName: string, params: Record<string, unknown>): string {
|
|
309
|
+
const parts: string[] = [toolName]
|
|
310
|
+
if (params.command && typeof params.command === 'string') {
|
|
311
|
+
parts.push(params.command)
|
|
312
|
+
}
|
|
313
|
+
if (params.file_path && typeof params.file_path === 'string') {
|
|
314
|
+
parts.push(params.file_path)
|
|
315
|
+
}
|
|
316
|
+
if (params.description && typeof params.description === 'string') {
|
|
317
|
+
parts.push(params.description)
|
|
318
|
+
}
|
|
319
|
+
if (params.error && typeof params.error === 'string') {
|
|
320
|
+
parts.push(params.error)
|
|
321
|
+
}
|
|
322
|
+
return parts.join(' ')
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIS Phase 2: Immune Memory Garbage Collection
|
|
3
|
+
*
|
|
4
|
+
* Periodic cleanup of the error signature database:
|
|
5
|
+
* - Removes retired signatures older than retention period
|
|
6
|
+
* - Merges near-duplicate signatures (same tool + category, similar pattern)
|
|
7
|
+
* - Auto-retires signatures with consistently zero success rate
|
|
8
|
+
*
|
|
9
|
+
* Designed to be called:
|
|
10
|
+
* - On daemon startup
|
|
11
|
+
* - Periodically (e.g. every 24h via ScheduleManager)
|
|
12
|
+
* - Manually via `/sis cleanup`
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ErrorSignatureDB, ErrorSignature } from './error-signature-db.js'
|
|
16
|
+
|
|
17
|
+
// ── Constants ──
|
|
18
|
+
|
|
19
|
+
/** Default retention period for retired signatures (days) */
|
|
20
|
+
const DEFAULT_RETENTION_DAYS = 90
|
|
21
|
+
|
|
22
|
+
/** Signatures with this many occurrences and 0 successes → auto-retire */
|
|
23
|
+
const ZERO_SUCCESS_THRESHOLD = 10
|
|
24
|
+
|
|
25
|
+
/** Levenshtein distance threshold for near-duplicate detection */
|
|
26
|
+
const SIMILARITY_THRESHOLD = 0.8
|
|
27
|
+
|
|
28
|
+
// ── Types ──
|
|
29
|
+
|
|
30
|
+
export interface GCReport {
|
|
31
|
+
/** Number of retired signatures removed */
|
|
32
|
+
retiredRemoved: number
|
|
33
|
+
/** Number of zero-success signatures auto-retired */
|
|
34
|
+
zeroSuccessRetired: number
|
|
35
|
+
/** Number of near-duplicate signatures merged */
|
|
36
|
+
duplicatesMerged: number
|
|
37
|
+
/** Total signatures before cleanup */
|
|
38
|
+
before: number
|
|
39
|
+
/** Total signatures after cleanup */
|
|
40
|
+
after: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── GC Engine ──
|
|
44
|
+
|
|
45
|
+
export class ImmuneMemoryGC {
|
|
46
|
+
private errorDB: ErrorSignatureDB
|
|
47
|
+
|
|
48
|
+
constructor(errorDB: ErrorSignatureDB) {
|
|
49
|
+
this.errorDB = errorDB
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run a full garbage collection cycle.
|
|
54
|
+
*
|
|
55
|
+
* @param retentionDays — days to retain retired signatures (default 90)
|
|
56
|
+
* @returns GCReport with cleanup statistics
|
|
57
|
+
*/
|
|
58
|
+
collect(retentionDays: number = DEFAULT_RETENTION_DAYS): GCReport {
|
|
59
|
+
const before = this.errorDB.getStats().total
|
|
60
|
+
|
|
61
|
+
// Phase 1: Remove old retired signatures
|
|
62
|
+
const retiredRemoved = this.errorDB.cleanup(retentionDays)
|
|
63
|
+
|
|
64
|
+
// Phase 2: Auto-retire zero-success signatures
|
|
65
|
+
const zeroSuccessRetired = this.retireZeroSuccess()
|
|
66
|
+
|
|
67
|
+
// Phase 3: Merge near-duplicates
|
|
68
|
+
const duplicatesMerged = this.mergeDuplicates()
|
|
69
|
+
|
|
70
|
+
const after = this.errorDB.getStats().total
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
retiredRemoved,
|
|
74
|
+
zeroSuccessRetired,
|
|
75
|
+
duplicatesMerged,
|
|
76
|
+
before,
|
|
77
|
+
after,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Private ──
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Auto-retire signatures that have been tried many times
|
|
85
|
+
* but never succeeded. These are likely bad or outdated fixes.
|
|
86
|
+
*/
|
|
87
|
+
private retireZeroSuccess(): number {
|
|
88
|
+
let count = 0
|
|
89
|
+
const active = this.errorDB.getActive()
|
|
90
|
+
|
|
91
|
+
for (const sig of active) {
|
|
92
|
+
if (
|
|
93
|
+
sig.occurrences >= ZERO_SUCCESS_THRESHOLD &&
|
|
94
|
+
sig.successCount === 0 &&
|
|
95
|
+
sig.status === 'active'
|
|
96
|
+
) {
|
|
97
|
+
this.errorDB.retire(sig.id)
|
|
98
|
+
count++
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return count
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Merge near-duplicate signatures.
|
|
107
|
+
* Two signatures are duplicates if they share the same toolName + category
|
|
108
|
+
* and their patterns are textually similar (high substring overlap).
|
|
109
|
+
*
|
|
110
|
+
* The older signature absorbs the newer one's occurrence count.
|
|
111
|
+
*/
|
|
112
|
+
private mergeDuplicates(): number {
|
|
113
|
+
let count = 0
|
|
114
|
+
const active = this.errorDB.getActive()
|
|
115
|
+
const merged = new Set<string>()
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < active.length; i++) {
|
|
118
|
+
const sigI = active[i]!
|
|
119
|
+
if (merged.has(sigI.id)) continue
|
|
120
|
+
|
|
121
|
+
for (let j = i + 1; j < active.length; j++) {
|
|
122
|
+
const sigJ = active[j]!
|
|
123
|
+
if (merged.has(sigJ.id)) continue
|
|
124
|
+
|
|
125
|
+
if (this.areSimilar(sigI, sigJ)) {
|
|
126
|
+
// Merge j into i (keep the older, more established one)
|
|
127
|
+
this.merge(sigI, sigJ)
|
|
128
|
+
merged.add(sigJ.id)
|
|
129
|
+
count++
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return count
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Check if two signatures are similar enough to be merged. */
|
|
138
|
+
private areSimilar(a: ErrorSignature, b: ErrorSignature): boolean {
|
|
139
|
+
// Must share toolName and category
|
|
140
|
+
if (a.toolName !== b.toolName) return false
|
|
141
|
+
if (a.category !== b.category) return false
|
|
142
|
+
|
|
143
|
+
// Check pattern similarity via substring containment
|
|
144
|
+
const shorter = a.pattern.length < b.pattern.length ? a.pattern : b.pattern
|
|
145
|
+
const longer = a.pattern.length < b.pattern.length ? b.pattern : a.pattern
|
|
146
|
+
|
|
147
|
+
if (longer.includes(shorter)) return true
|
|
148
|
+
|
|
149
|
+
// Check Levenshtein-like similarity
|
|
150
|
+
const similarity = this.jaccardSimilarity(shorter, longer)
|
|
151
|
+
return similarity >= SIMILARITY_THRESHOLD
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Jaccard similarity on word tokens (simple and fast). */
|
|
155
|
+
private jaccardSimilarity(a: string, b: string): number {
|
|
156
|
+
const tokensA = new Set(a.toLowerCase().split(/\s+/))
|
|
157
|
+
const tokensB = new Set(b.toLowerCase().split(/\s+/))
|
|
158
|
+
|
|
159
|
+
let intersection = 0
|
|
160
|
+
for (const t of tokensA) {
|
|
161
|
+
if (tokensB.has(t)) intersection++
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const union = tokensA.size + tokensB.size - intersection
|
|
165
|
+
return union > 0 ? intersection / union : 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Merge signature `from` into `to`. Retire `from`. */
|
|
169
|
+
private merge(to: ErrorSignature, from: ErrorSignature): void {
|
|
170
|
+
// Transfer occurrences
|
|
171
|
+
to.occurrences += from.occurrences
|
|
172
|
+
to.successCount += from.successCount
|
|
173
|
+
to.successRate = to.occurrences > 0 ? to.successCount / to.occurrences : 1.0
|
|
174
|
+
|
|
175
|
+
// Keep the earlier firstSeen
|
|
176
|
+
if (from.firstSeen < to.firstSeen) {
|
|
177
|
+
to.firstSeen = from.firstSeen
|
|
178
|
+
}
|
|
179
|
+
// Keep the later lastSeen
|
|
180
|
+
if (from.lastSeen > to.lastSeen) {
|
|
181
|
+
to.lastSeen = from.lastSeen
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Retire the absorbed signature
|
|
185
|
+
this.errorDB.retire(from.id)
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/core/instructions.ts
CHANGED
|
@@ -139,6 +139,20 @@ When a workflow completes successfully, offer to save it:
|
|
|
139
139
|
Script format: export const meta = { name, description, phases: [...] }
|
|
140
140
|
// script body using primitives...`)
|
|
141
141
|
|
|
142
|
+
// Inject code search conventions — prefer dedicated tools over raw Bash
|
|
143
|
+
parts.push(`## Code Search Conventions
|
|
144
|
+
|
|
145
|
+
- Prefer the **Grep** tool for file-content search — it runs ripgrep (rg),
|
|
146
|
+
10× faster than grep, with automatic fallback to grep if rg is unavailable
|
|
147
|
+
- Use the **Glob** tool to find files by name pattern, then Grep to search
|
|
148
|
+
their contents — narrow scope before full-text search
|
|
149
|
+
- Use **Bash** with grep/rg/find ONLY for complex multi-step pipelines
|
|
150
|
+
(e.g., pipe to sort | uniq -c | sort -rn, or chained find + xargs)
|
|
151
|
+
- Grep tool handles rg→grep fallback automatically; no need to
|
|
152
|
+
pre-check for rg availability or manually fall back
|
|
153
|
+
- When you need to search AND read results: Glob → Grep → Read
|
|
154
|
+
(find files, search contents, then read the matching files)`)
|
|
155
|
+
|
|
142
156
|
return parts.join('\n\n---\n\n')
|
|
143
157
|
}
|
|
144
158
|
|