@miphamai/cli 0.22.0 → 0.24.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/README.md +1 -1
- package/package.json +1 -1
- package/skills/workflows/audit.js +66 -18
- package/src/agent/agent-context.ts +20 -57
- package/src/agent/agent-experience.ts +8 -0
- package/src/agent/cross-session/discovery.ts +99 -0
- package/src/agent/cross-session/file-inbox.ts +118 -0
- package/src/agent/cross-session/transport.ts +17 -0
- package/src/agent/effectiveness-tracker.ts +149 -0
- package/src/agent/experience-rules.ts +188 -0
- package/src/agent/message-router.ts +83 -0
- package/src/agent/pattern-analyzer.ts +154 -0
- package/src/agent/sub-agent.ts +67 -0
- package/src/agent/types.ts +2 -0
- package/src/config/defaults.ts +12 -0
- package/src/config/loader.ts +33 -0
- package/src/core/credential-masker/env-filter.ts +39 -0
- package/src/core/credential-masker/index.ts +35 -0
- package/src/core/credential-masker/matcher.ts +106 -0
- package/src/core/credential-masker/output-scrub.ts +27 -0
- package/src/core/credential-masker/pipeline.ts +67 -0
- package/src/core/credential-masker/strategies/aws.ts +50 -0
- package/src/core/credential-masker/strategies/extract.ts +106 -0
- package/src/core/credential-masker/strategies/full.ts +15 -0
- package/src/core/credential-masker/strategies/jwt.ts +75 -0
- package/src/core/credential-masker/types.ts +16 -0
- package/src/core/credential-masker.ts +12 -187
- package/src/core/engine.ts +114 -10
- package/src/core/rule-engine.ts +179 -0
- package/src/core/session-store.ts +15 -2
- package/src/index.tsx +47 -2
- package/src/shared/types.ts +69 -5
- package/src/tools/agent/agent.ts +7 -1
- package/src/tools/agent/list-agents.ts +67 -0
- package/src/tools/agent/send-message.ts +21 -24
- package/src/tools/exec/bash.ts +67 -3
- package/src/tools/index.ts +2 -0
- package/src/ui/commands.ts +133 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export interface ExperienceRule {
|
|
2
|
+
id: string // rule-<category>-<shortHash>
|
|
3
|
+
type: 'mandatory' | 'warning'
|
|
4
|
+
condition: string
|
|
5
|
+
action: string
|
|
6
|
+
evidence: {
|
|
7
|
+
failureCount: number
|
|
8
|
+
lastFailure: string // ISO date
|
|
9
|
+
examples: string[]
|
|
10
|
+
}
|
|
11
|
+
category: 'timeout' | 'import' | 'search' | 'tool-params' | 'semantic'
|
|
12
|
+
source: 'agent-experience' | 'manual' | 'pattern-analyzer'
|
|
13
|
+
agentName: string
|
|
14
|
+
createdAt: string // ISO date
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FailureEntry {
|
|
18
|
+
date: string
|
|
19
|
+
description: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFailureEntries(content: string): FailureEntry[] {
|
|
23
|
+
const entries: FailureEntry[] = []
|
|
24
|
+
const failureIdx = content.indexOf('## Failure Patterns')
|
|
25
|
+
if (failureIdx === -1) return entries
|
|
26
|
+
|
|
27
|
+
const afterFailure = content.slice(failureIdx)
|
|
28
|
+
const nextSection = afterFailure.indexOf('\n## ', '## Failure Patterns'.length)
|
|
29
|
+
const section = nextSection !== -1 ? afterFailure.slice(0, nextSection) : afterFailure
|
|
30
|
+
|
|
31
|
+
const lines = section.split('\n')
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const match = line.match(/^- \[(\d{4}-\d{2}-\d{2})\]\s+(.+)/)
|
|
34
|
+
if (match && match[1] && match[2]) {
|
|
35
|
+
entries.push({ date: match[1], description: match[2].trim() })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function categorize(description: string): ExperienceRule['category'] {
|
|
42
|
+
const lower = description.toLowerCase()
|
|
43
|
+
if (lower.includes('timeout')) return 'timeout'
|
|
44
|
+
if (/module_not_found|import|\.js/.test(lower)) return 'import'
|
|
45
|
+
if (/grep|token.*overflow|search.*scope/.test(lower)) return 'search'
|
|
46
|
+
if (/bash|command.*fail|docker|npm|pnpm|cargo|brew/.test(lower)) return 'tool-params'
|
|
47
|
+
return 'semantic'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createRuleId(category: string, description: string): string {
|
|
51
|
+
// Hash the first 30 chars of description for uniqueness
|
|
52
|
+
const hash = description
|
|
53
|
+
.slice(0, 30)
|
|
54
|
+
.split('')
|
|
55
|
+
.reduce((acc, c) => {
|
|
56
|
+
return ((acc << 5) - acc + c.charCodeAt(0)) | 0
|
|
57
|
+
}, 0)
|
|
58
|
+
.toString(36)
|
|
59
|
+
.slice(-6)
|
|
60
|
+
return `rule-${category}-${hash}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function conditionForCategory(category: string, entries: FailureEntry[]): string {
|
|
64
|
+
const combined = entries.map((e) => e.description).join('; ')
|
|
65
|
+
switch (category) {
|
|
66
|
+
case 'timeout':
|
|
67
|
+
return 'heavy CLI commands (npm/docker/pnpm)'
|
|
68
|
+
case 'import':
|
|
69
|
+
return 'ESM module imports missing .js extension'
|
|
70
|
+
case 'search':
|
|
71
|
+
return 'full-repository Grep without directory scoping'
|
|
72
|
+
case 'tool-params':
|
|
73
|
+
return combined.slice(0, 100)
|
|
74
|
+
default:
|
|
75
|
+
return combined.slice(0, 100)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function actionForCategory(category: string): string {
|
|
80
|
+
switch (category) {
|
|
81
|
+
case 'timeout':
|
|
82
|
+
return 'set Bash timeout ≥ 300000ms for heavy commands'
|
|
83
|
+
case 'import':
|
|
84
|
+
return 'always append .js extension to ESM relative imports'
|
|
85
|
+
case 'search':
|
|
86
|
+
return 'use Glob to narrow directory before Grep'
|
|
87
|
+
case 'tool-params':
|
|
88
|
+
return 'review and adjust tool parameters before execution'
|
|
89
|
+
default:
|
|
90
|
+
return 'verify tool parameters match known good patterns'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class ExperienceRuleExtractor {
|
|
95
|
+
extract(content: string, agentName: string): ExperienceRule[] {
|
|
96
|
+
if (!content || !content.includes('## Failure Patterns')) return []
|
|
97
|
+
|
|
98
|
+
const entries = parseFailureEntries(content)
|
|
99
|
+
if (entries.length < 2) return []
|
|
100
|
+
|
|
101
|
+
// Group by category
|
|
102
|
+
const byCategory = new Map<string, FailureEntry[]>()
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const cat = categorize(entry.description)
|
|
105
|
+
const existing = byCategory.get(cat) || []
|
|
106
|
+
existing.push(entry)
|
|
107
|
+
byCategory.set(cat, existing)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rules: ExperienceRule[] = []
|
|
111
|
+
for (const [category, catEntries] of byCategory) {
|
|
112
|
+
if (catEntries.length < 2) continue
|
|
113
|
+
|
|
114
|
+
const type: 'mandatory' | 'warning' = catEntries.length >= 3 ? 'mandatory' : 'warning'
|
|
115
|
+
const firstEntry = catEntries[0]
|
|
116
|
+
const lastEntry = catEntries[catEntries.length - 1]
|
|
117
|
+
if (!firstEntry || !lastEntry) continue
|
|
118
|
+
|
|
119
|
+
rules.push({
|
|
120
|
+
id: createRuleId(category, firstEntry.description),
|
|
121
|
+
type,
|
|
122
|
+
condition: conditionForCategory(category, catEntries),
|
|
123
|
+
action: actionForCategory(category),
|
|
124
|
+
evidence: {
|
|
125
|
+
failureCount: catEntries.length,
|
|
126
|
+
lastFailure: lastEntry.date,
|
|
127
|
+
examples: catEntries.slice(0, 3).map((e) => e.description),
|
|
128
|
+
},
|
|
129
|
+
category: category as ExperienceRule['category'],
|
|
130
|
+
source: 'agent-experience',
|
|
131
|
+
agentName,
|
|
132
|
+
createdAt: new Date().toISOString().slice(0, 10),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return rules
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
prioritize(rules: ExperienceRule[]): ExperienceRule[] {
|
|
140
|
+
const order: Record<string, number> = {
|
|
141
|
+
mandatory: 0,
|
|
142
|
+
warning: 1,
|
|
143
|
+
}
|
|
144
|
+
return [...rules].sort((a, b) => {
|
|
145
|
+
const aOrder = order[a.type] ?? 0
|
|
146
|
+
const bOrder = order[b.type] ?? 0
|
|
147
|
+
const typeDiff = aOrder - bOrder
|
|
148
|
+
if (typeDiff !== 0) return typeDiff
|
|
149
|
+
// Within same type, more failures first
|
|
150
|
+
return b.evidence.failureCount - a.evidence.failureCount
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
formatForInjection(rules: ExperienceRule[]): string {
|
|
155
|
+
if (rules.length === 0) return ''
|
|
156
|
+
|
|
157
|
+
const mandatory = rules.filter((r) => r.type === 'mandatory')
|
|
158
|
+
const warnings = rules.filter((r) => r.type === 'warning')
|
|
159
|
+
|
|
160
|
+
let output = ''
|
|
161
|
+
|
|
162
|
+
if (mandatory.length > 0) {
|
|
163
|
+
output += '## ⚠️ Active Mandatory Rules (learned from past failures)\n\n'
|
|
164
|
+
for (const r of mandatory) {
|
|
165
|
+
const idx = mandatory.indexOf(r) + 1
|
|
166
|
+
output += `${idx}. [${r.category}] ${r.condition} → ${r.action}\n`
|
|
167
|
+
output += ` Evidence: ${r.evidence.failureCount} failures`
|
|
168
|
+
if (r.evidence.lastFailure) output += `, last: ${r.evidence.lastFailure}`
|
|
169
|
+
output += '\n'
|
|
170
|
+
if (r.evidence.examples.length > 0) {
|
|
171
|
+
output += ` Example: ${r.evidence.examples[0]}\n`
|
|
172
|
+
}
|
|
173
|
+
output += '\n'
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (warnings.length > 0) {
|
|
178
|
+
output += '## ⚡ Observed Patterns (warning level)\n\n'
|
|
179
|
+
for (const r of warnings) {
|
|
180
|
+
const idx = warnings.indexOf(r) + 1
|
|
181
|
+
output += `${idx}. [${r.category}] ${r.condition} → ${r.action}\n`
|
|
182
|
+
output += ` Evidence: ${r.evidence.failureCount} occurrences\n\n`
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return output.trim()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getMessageBus } from './message-bus'
|
|
2
|
+
import { getFileInboxTransport } from './cross-session/file-inbox'
|
|
3
|
+
import { discoverSessions, createSessionInfo } from './cross-session/discovery'
|
|
4
|
+
import type { AgentMessage } from './message-bus'
|
|
5
|
+
|
|
6
|
+
export interface RouteResult {
|
|
7
|
+
success: boolean
|
|
8
|
+
routedTo: 'bus' | 'inbox' | 'unknown'
|
|
9
|
+
error?: string
|
|
10
|
+
messageId?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MessageRouter decides how to deliver a message based on the recipient.
|
|
15
|
+
*
|
|
16
|
+
* - "main" or background task IDs → in-memory AgentMessageBus (same process)
|
|
17
|
+
* - Session IDs matching discoverable sessions → file inbox (cross-session)
|
|
18
|
+
* - Unknown → error
|
|
19
|
+
*/
|
|
20
|
+
export class MessageRouter {
|
|
21
|
+
/**
|
|
22
|
+
* Route a message to the appropriate transport.
|
|
23
|
+
*/
|
|
24
|
+
async route(from: string, to: string, summary: string, message: string): Promise<RouteResult> {
|
|
25
|
+
// Same-process routing: "main" or background task IDs
|
|
26
|
+
if (to === 'main' || to.startsWith('bg-') || to.startsWith('sub-agent-')) {
|
|
27
|
+
try {
|
|
28
|
+
const bus = getMessageBus()
|
|
29
|
+
const msgId = bus.post(from, to, summary, message)
|
|
30
|
+
return { success: true, routedTo: 'bus', messageId: msgId }
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { success: false, routedTo: 'bus', error: String(err) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cross-session routing
|
|
37
|
+
const sessions = discoverSessions()
|
|
38
|
+
const targetSession = sessions.find((s) => s.id === to)
|
|
39
|
+
|
|
40
|
+
if (!targetSession) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
routedTo: 'unknown',
|
|
44
|
+
error: `No active session found with ID "${to}". Use ListAgents to discover available sessions.`,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get sender info
|
|
49
|
+
const senderSession = createSessionInfo(from, from)
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const transport = getFileInboxTransport()
|
|
53
|
+
const msg: AgentMessage = {
|
|
54
|
+
id: `xmsg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
55
|
+
from,
|
|
56
|
+
to,
|
|
57
|
+
summary: summary.slice(0, 200),
|
|
58
|
+
message,
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
read: false,
|
|
61
|
+
type: 'message',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const delivered = await transport.send(senderSession, to, msg)
|
|
65
|
+
if (!delivered) {
|
|
66
|
+
return { success: false, routedTo: 'inbox', error: 'Failed to write message to inbox.' }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { success: true, routedTo: 'inbox', messageId: msg.id }
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { success: false, routedTo: 'inbox', error: String(err) }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let _router: MessageRouter | null = null
|
|
77
|
+
|
|
78
|
+
export function getMessageRouter(): MessageRouter {
|
|
79
|
+
if (!_router) {
|
|
80
|
+
_router = new MessageRouter()
|
|
81
|
+
}
|
|
82
|
+
return _router
|
|
83
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { AgentExperience } from './agent-experience.js'
|
|
5
|
+
import { parseFailureEntries, categorize, type ExperienceRule } from './experience-rules.js'
|
|
6
|
+
import type { ToolRule } from '../core/rule-engine.js'
|
|
7
|
+
|
|
8
|
+
export interface Pattern {
|
|
9
|
+
id: string
|
|
10
|
+
category: 'timeout' | 'import' | 'search' | 'tool-params' | 'semantic'
|
|
11
|
+
agentName: string
|
|
12
|
+
frequency: number
|
|
13
|
+
confidence: 'high' | 'medium' | 'low'
|
|
14
|
+
examples: string[]
|
|
15
|
+
firstSeen: string
|
|
16
|
+
lastSeen: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PatternAnalyzer {
|
|
20
|
+
analyzeAgent(agentName: string, baseDir?: string): Pattern[] {
|
|
21
|
+
const dir = baseDir || join(homedir(), '.mipham', 'agent-memory')
|
|
22
|
+
const exp = new AgentExperience(agentName, dir)
|
|
23
|
+
const content = exp.getExperience()
|
|
24
|
+
if (!content) return []
|
|
25
|
+
|
|
26
|
+
const allEntries = parseFailureEntries(content)
|
|
27
|
+
|
|
28
|
+
// Group by category
|
|
29
|
+
const byCategory = new Map<string, { descriptions: string[]; dates: string[] }>()
|
|
30
|
+
for (const entry of allEntries) {
|
|
31
|
+
const cat = categorize(entry.description)
|
|
32
|
+
const existing = byCategory.get(cat) || { descriptions: [], dates: [] }
|
|
33
|
+
existing.descriptions.push(entry.description)
|
|
34
|
+
existing.dates.push(entry.date)
|
|
35
|
+
byCategory.set(cat, existing)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const patterns: Pattern[] = []
|
|
39
|
+
for (const [category, data] of byCategory) {
|
|
40
|
+
if (data.descriptions.length < 3) continue
|
|
41
|
+
|
|
42
|
+
patterns.push({
|
|
43
|
+
id: `pattern-${category}-${agentName}`,
|
|
44
|
+
category: category as Pattern['category'],
|
|
45
|
+
agentName,
|
|
46
|
+
frequency: data.descriptions.length,
|
|
47
|
+
confidence: data.descriptions.length >= 5 ? 'high' : 'medium',
|
|
48
|
+
examples: data.descriptions.slice(0, 5),
|
|
49
|
+
firstSeen: data.dates[0] ?? '',
|
|
50
|
+
lastSeen: data.dates[data.dates.length - 1] ?? '',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return patterns
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
analyzeAllAgents(baseDir?: string): Pattern[] {
|
|
58
|
+
const dir = baseDir || join(homedir(), '.mipham', 'agent-memory')
|
|
59
|
+
if (!existsSync(dir)) return []
|
|
60
|
+
|
|
61
|
+
let agents: string[]
|
|
62
|
+
try {
|
|
63
|
+
agents = readdirSync(dir, { withFileTypes: true })
|
|
64
|
+
.filter((d) => d.isDirectory())
|
|
65
|
+
.map((d) => d.name)
|
|
66
|
+
} catch {
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const allPatterns: Pattern[] = []
|
|
71
|
+
for (const agent of agents) {
|
|
72
|
+
allPatterns.push(...this.analyzeAgent(agent, baseDir))
|
|
73
|
+
}
|
|
74
|
+
return allPatterns
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toRule(pattern: Pattern): ExperienceRule {
|
|
78
|
+
return {
|
|
79
|
+
id: `${pattern.id}-${Date.now().toString(36)}`,
|
|
80
|
+
type: pattern.frequency >= 3 ? 'mandatory' : 'warning',
|
|
81
|
+
condition: this._conditionForCategory(pattern),
|
|
82
|
+
action: this._actionForCategory(pattern.category),
|
|
83
|
+
evidence: {
|
|
84
|
+
failureCount: pattern.frequency,
|
|
85
|
+
lastFailure: pattern.lastSeen,
|
|
86
|
+
examples: pattern.examples.slice(0, 3),
|
|
87
|
+
},
|
|
88
|
+
category: pattern.category,
|
|
89
|
+
source: 'pattern-analyzer',
|
|
90
|
+
agentName: pattern.agentName,
|
|
91
|
+
createdAt: new Date().toISOString().slice(0, 10),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
toToolRule(pattern: Pattern): ToolRule {
|
|
96
|
+
const toolNameMap: Record<string, string> = {
|
|
97
|
+
timeout: 'Bash',
|
|
98
|
+
'tool-params': 'Bash',
|
|
99
|
+
import: 'Write',
|
|
100
|
+
search: 'Grep',
|
|
101
|
+
semantic: 'Bash',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: pattern.id,
|
|
106
|
+
toolName: toolNameMap[pattern.category] || 'Bash',
|
|
107
|
+
category: pattern.category,
|
|
108
|
+
match: (p: Record<string, unknown>): boolean => {
|
|
109
|
+
if (pattern.category === 'timeout') {
|
|
110
|
+
const cmd = String(p.command ?? '')
|
|
111
|
+
const heavy = /npm|docker|pnpm|cargo|brew|install|build/.test(cmd)
|
|
112
|
+
if (!heavy) return false
|
|
113
|
+
const timeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
114
|
+
return !timeout || timeout < 300_000
|
|
115
|
+
}
|
|
116
|
+
return true
|
|
117
|
+
},
|
|
118
|
+
fix: (p: Record<string, unknown>) => ({
|
|
119
|
+
modified: pattern.category === 'timeout' ? { ...p, timeout: 300_000 } : p,
|
|
120
|
+
warning: `🤖 [auto-rule] ${pattern.category}: ${pattern.frequency} 次同类失败 — ${this._actionForCategory(pattern.category)}`,
|
|
121
|
+
}),
|
|
122
|
+
source: 'pattern-analyzer',
|
|
123
|
+
enabled: true,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Private helpers ──
|
|
128
|
+
|
|
129
|
+
private _conditionForCategory(pattern: Pattern): string {
|
|
130
|
+
switch (pattern.category) {
|
|
131
|
+
case 'timeout':
|
|
132
|
+
return 'heavy CLI commands (npm/docker/pnpm/cargo)'
|
|
133
|
+
case 'import':
|
|
134
|
+
return 'ESM module imports missing .js extension'
|
|
135
|
+
case 'search':
|
|
136
|
+
return 'full-repository search without directory scoping'
|
|
137
|
+
default:
|
|
138
|
+
return pattern.examples[0]?.slice(0, 100) || 'unknown condition'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private _actionForCategory(category: string): string {
|
|
143
|
+
switch (category) {
|
|
144
|
+
case 'timeout':
|
|
145
|
+
return 'set Bash timeout ≥ 300000ms for heavy commands'
|
|
146
|
+
case 'import':
|
|
147
|
+
return 'append .js extension to ESM relative imports'
|
|
148
|
+
case 'search':
|
|
149
|
+
return 'use Glob to narrow directory before Grep'
|
|
150
|
+
default:
|
|
151
|
+
return 'review and adjust tool parameters before execution'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -7,6 +7,16 @@ import { getMessageBus } from './message-bus'
|
|
|
7
7
|
import type { HookEngine } from '../core/hooks'
|
|
8
8
|
import type { PermissionSystem } from '../core/permission'
|
|
9
9
|
import { AgentExperience } from './agent-experience'
|
|
10
|
+
import { PatternAnalyzer } from './pattern-analyzer.js'
|
|
11
|
+
import type { ExperienceRuleEngine } from '../core/rule-engine.js'
|
|
12
|
+
|
|
13
|
+
// Singleton instances (created lazily)
|
|
14
|
+
let _patternAnalyzer: PatternAnalyzer | undefined
|
|
15
|
+
|
|
16
|
+
function getPatternAnalyzer(): PatternAnalyzer {
|
|
17
|
+
if (!_patternAnalyzer) _patternAnalyzer = new PatternAnalyzer()
|
|
18
|
+
return _patternAnalyzer
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
|
|
12
22
|
general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
|
|
@@ -32,6 +42,7 @@ export class SubAgent {
|
|
|
32
42
|
private toolRegistry: Map<string, ToolDefinition>,
|
|
33
43
|
private permission?: PermissionSystem,
|
|
34
44
|
private hookEngine?: HookEngine,
|
|
45
|
+
private ruleEngine?: ExperienceRuleEngine,
|
|
35
46
|
) {}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -85,6 +96,24 @@ export class SubAgent {
|
|
|
85
96
|
options.agentDef,
|
|
86
97
|
)
|
|
87
98
|
}
|
|
99
|
+
|
|
100
|
+
// CRSI: trigger pattern analysis after each agent execution
|
|
101
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
102
|
+
if (options.autoPatternAnalysis !== false) {
|
|
103
|
+
try {
|
|
104
|
+
const analyzer = getPatternAnalyzer()
|
|
105
|
+
const agentName = options.agentDef?.name || agentType
|
|
106
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
107
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
108
|
+
for (const pattern of patterns) {
|
|
109
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
110
|
+
this.ruleEngine.register(toolRule)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Pattern analysis failure never blocks agent execution
|
|
115
|
+
}
|
|
116
|
+
}
|
|
88
117
|
})
|
|
89
118
|
|
|
90
119
|
return `[background-task:${taskId}]`
|
|
@@ -97,6 +126,25 @@ export class SubAgent {
|
|
|
97
126
|
await this.hookEngine.executeSubagentStop(agentType, description, 'sub-agent', true, result)
|
|
98
127
|
}
|
|
99
128
|
this.logSuccessExperience(agentType, description, result, options.agentDef)
|
|
129
|
+
|
|
130
|
+
// CRSI: trigger pattern analysis after successful sync execution
|
|
131
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
132
|
+
if (options.autoPatternAnalysis !== false) {
|
|
133
|
+
try {
|
|
134
|
+
const analyzer = getPatternAnalyzer()
|
|
135
|
+
const agentName = options.agentDef?.name || agentType
|
|
136
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
137
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
138
|
+
for (const pattern of patterns) {
|
|
139
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
140
|
+
this.ruleEngine.register(toolRule)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Pattern analysis failure never blocks agent execution
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
100
148
|
return result
|
|
101
149
|
} catch (err) {
|
|
102
150
|
if (this.hookEngine) {
|
|
@@ -109,6 +157,25 @@ export class SubAgent {
|
|
|
109
157
|
)
|
|
110
158
|
}
|
|
111
159
|
this.logFailureExperience(agentType, description, String(err), options.agentDef)
|
|
160
|
+
|
|
161
|
+
// CRSI: trigger pattern analysis after failed sync execution (failures produce patterns too)
|
|
162
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
163
|
+
if (options.autoPatternAnalysis !== false) {
|
|
164
|
+
try {
|
|
165
|
+
const analyzer = getPatternAnalyzer()
|
|
166
|
+
const agentName = options.agentDef?.name || agentType
|
|
167
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
168
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
169
|
+
for (const pattern of patterns) {
|
|
170
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
171
|
+
this.ruleEngine.register(toolRule)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Pattern analysis failure never blocks agent execution
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
throw err
|
|
113
180
|
}
|
|
114
181
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -46,4 +46,6 @@ export interface SubAgentOptions {
|
|
|
46
46
|
onProgress?: (chunk: string) => void
|
|
47
47
|
/** When set, tool executions use this path as cwd (git worktree isolation). */
|
|
48
48
|
worktreePath?: string
|
|
49
|
+
/** CRSI: when false, skip pattern analysis after agent execution. Default true. */
|
|
50
|
+
autoPatternAnalysis?: boolean
|
|
49
51
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
InferenceHookConfig,
|
|
4
4
|
CredentialMaskingConfig,
|
|
5
5
|
BackgroundAgentConfig,
|
|
6
|
+
CrossSessionConfig,
|
|
6
7
|
} from '../shared/index.ts'
|
|
7
8
|
import { DEFAULT_PROVIDERS } from '../shared/index.ts'
|
|
8
9
|
import { PACKAGE_VERSION } from '../shared/index.ts'
|
|
@@ -21,6 +22,12 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
21
22
|
mcp: { oauthEnabled: true },
|
|
22
23
|
context: { useRealTokenizer: true, adaptiveThresholds: true },
|
|
23
24
|
},
|
|
25
|
+
crsi: {
|
|
26
|
+
ruleInjection: true,
|
|
27
|
+
preToolHook: true,
|
|
28
|
+
autoPatternAnalysis: true,
|
|
29
|
+
autoRuleManagement: true,
|
|
30
|
+
},
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
|
@@ -51,3 +58,8 @@ export const DEFAULT_BACKGROUND_AGENT_CONFIG: BackgroundAgentConfig = {
|
|
|
51
58
|
auto_worktree: true,
|
|
52
59
|
commit_coauthors: true,
|
|
53
60
|
}
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_CROSS_SESSION_CONFIG: CrossSessionConfig = {
|
|
63
|
+
crossSessionInbound: 'ask',
|
|
64
|
+
dialogExpiry: 300,
|
|
65
|
+
}
|
package/src/config/loader.ts
CHANGED
|
@@ -17,12 +17,14 @@ import type {
|
|
|
17
17
|
InferenceHookConfig,
|
|
18
18
|
CredentialMaskingConfig,
|
|
19
19
|
BackgroundAgentConfig,
|
|
20
|
+
CrossSessionConfig,
|
|
20
21
|
} from '../shared/index.ts'
|
|
21
22
|
import {
|
|
22
23
|
DEFAULT_CONFIG,
|
|
23
24
|
DEFAULT_INFERENCE_HOOK_CONFIG,
|
|
24
25
|
DEFAULT_CREDENTIAL_MASKING_CONFIG,
|
|
25
26
|
DEFAULT_BACKGROUND_AGENT_CONFIG,
|
|
27
|
+
DEFAULT_CROSS_SESSION_CONFIG,
|
|
26
28
|
} from './defaults'
|
|
27
29
|
|
|
28
30
|
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
@@ -376,3 +378,34 @@ export function loadBackgroundAgentConfig(cwd: string = process.cwd()): Backgrou
|
|
|
376
378
|
|
|
377
379
|
return merged
|
|
378
380
|
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Load cross-session messaging configuration from the same config sources.
|
|
384
|
+
* Merges project-level over user-level. Returns defaults if no section present.
|
|
385
|
+
*/
|
|
386
|
+
export function loadCrossSessionConfig(cwd: string = process.cwd()): CrossSessionConfig {
|
|
387
|
+
const configPath = join(cwd, '.mipham', 'config.yml')
|
|
388
|
+
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
389
|
+
|
|
390
|
+
let merged = { ...DEFAULT_CROSS_SESSION_CONFIG }
|
|
391
|
+
|
|
392
|
+
const paths = [userConfigPath, configPath] // project wins (loaded last)
|
|
393
|
+
for (const path of paths) {
|
|
394
|
+
try {
|
|
395
|
+
if (!existsSync(path)) continue
|
|
396
|
+
const raw = readFileSync(path, 'utf-8')
|
|
397
|
+
const parsed = parseYaml(raw) as Record<string, unknown>
|
|
398
|
+
const section = parsed.cross_session as Partial<CrossSessionConfig> | undefined
|
|
399
|
+
if (section) {
|
|
400
|
+
merged = {
|
|
401
|
+
crossSessionInbound: section.crossSessionInbound ?? merged.crossSessionInbound,
|
|
402
|
+
dialogExpiry: section.dialogExpiry ?? merged.dialogExpiry,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// Silently skip malformed configs
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return merged
|
|
411
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CREDENTIAL_SENTINEL } from './types'
|
|
2
|
+
import type { CredentialMaskingConfig } from '../../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filter sensitive environment variables before spawning a subprocess.
|
|
6
|
+
* Returns a sanitized copy of process.env with matching vars removed.
|
|
7
|
+
*/
|
|
8
|
+
export function filterEnv(
|
|
9
|
+
env: Record<string, string | undefined>,
|
|
10
|
+
config: CredentialMaskingConfig,
|
|
11
|
+
): Record<string, string | undefined> {
|
|
12
|
+
if (!config.enabled || !config.env_filter.enabled) return { ...env }
|
|
13
|
+
|
|
14
|
+
const filtered: Record<string, string | undefined> = {}
|
|
15
|
+
for (const [key, value] of Object.entries(env)) {
|
|
16
|
+
let blocked = false
|
|
17
|
+
for (const pattern of config.env_filter.patterns) {
|
|
18
|
+
try {
|
|
19
|
+
// Strip (?i) inline flags — JS uses the 'i' flag instead
|
|
20
|
+
const clean = pattern.replace(/^\(\?i\)/, '')
|
|
21
|
+
const regex = new RegExp(clean, 'i')
|
|
22
|
+
if (regex.test(key)) {
|
|
23
|
+
blocked = true
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Invalid regex — skip
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (blocked) {
|
|
31
|
+
// Replace with sentinel so tools can detect they're filtered
|
|
32
|
+
filtered[key] = CREDENTIAL_SENTINEL
|
|
33
|
+
} else {
|
|
34
|
+
filtered[key] = value
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return filtered
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { CREDENTIAL_SENTINEL } from './types'
|
|
2
|
+
export type { MaskingStrategy } from './types'
|
|
3
|
+
|
|
4
|
+
export { MaskingPipeline, getPipeline, maskContent } from './pipeline'
|
|
5
|
+
export { matchCredentialFile, matchPath, globToRegex } from './matcher'
|
|
6
|
+
export { filterEnv } from './env-filter'
|
|
7
|
+
export { maskOutput } from './output-scrub'
|
|
8
|
+
|
|
9
|
+
// Strategies
|
|
10
|
+
export { FullMaskingStrategy } from './strategies/full'
|
|
11
|
+
export { ExtractMaskingStrategy } from './strategies/extract'
|
|
12
|
+
export { JwtMaskingStrategy } from './strategies/jwt'
|
|
13
|
+
export { AwsMaskingStrategy } from './strategies/aws'
|
|
14
|
+
|
|
15
|
+
import { getPipeline } from './pipeline'
|
|
16
|
+
import { FullMaskingStrategy } from './strategies/full'
|
|
17
|
+
import { ExtractMaskingStrategy } from './strategies/extract'
|
|
18
|
+
import { JwtMaskingStrategy } from './strategies/jwt'
|
|
19
|
+
import { AwsMaskingStrategy } from './strategies/aws'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the masking pipeline with all built-in strategies.
|
|
23
|
+
* Call once at startup. Safe to call multiple times (idempotent).
|
|
24
|
+
*/
|
|
25
|
+
let _initialized = false
|
|
26
|
+
|
|
27
|
+
export function initializePipeline(): void {
|
|
28
|
+
if (_initialized) return
|
|
29
|
+
const pipeline = getPipeline()
|
|
30
|
+
pipeline.register(new FullMaskingStrategy())
|
|
31
|
+
pipeline.register(new ExtractMaskingStrategy())
|
|
32
|
+
pipeline.register(new JwtMaskingStrategy())
|
|
33
|
+
pipeline.register(new AwsMaskingStrategy())
|
|
34
|
+
_initialized = true
|
|
35
|
+
}
|