@miphamai/cli 0.23.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/package.json +1 -1
- package/src/agent/agent-context.ts +20 -57
- package/src/agent/agent-experience.ts +8 -0
- package/src/agent/effectiveness-tracker.ts +149 -0
- package/src/agent/experience-rules.ts +188 -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 +6 -0
- package/src/core/engine.ts +70 -1
- package/src/core/rule-engine.ts +179 -0
- package/src/index.tsx +3 -1
- package/src/shared/types.ts +14 -0
- package/src/tools/agent/agent.ts +7 -1
- package/src/ui/commands.ts +133 -0
package/package.json
CHANGED
|
@@ -5,7 +5,9 @@ import { homedir } from 'node:os'
|
|
|
5
5
|
import { ContextManager } from '../core/context'
|
|
6
6
|
import type { ToolDefinition } from '../shared/index.ts'
|
|
7
7
|
import type { AgentDefinition } from './types'
|
|
8
|
+
import type { CrsiConfig } from '../shared/index.ts'
|
|
8
9
|
import { AgentExperience } from './agent-experience'
|
|
10
|
+
import { ExperienceRuleExtractor } from './experience-rules.js'
|
|
9
11
|
|
|
10
12
|
export interface AgentContextResult {
|
|
11
13
|
context: ContextManager
|
|
@@ -15,8 +17,14 @@ export interface AgentContextResult {
|
|
|
15
17
|
/**
|
|
16
18
|
* Load agent memory files from the appropriate scope directory.
|
|
17
19
|
* Returns combined content for injection into the system prompt.
|
|
20
|
+
*
|
|
21
|
+
* @param crsiRuleInjection — when false, skip experience rule extraction (CRSI feature flag).
|
|
18
22
|
*/
|
|
19
|
-
function loadAgentMemory(
|
|
23
|
+
function loadAgentMemory(
|
|
24
|
+
agentName: string,
|
|
25
|
+
scope: 'user' | 'project' | 'local',
|
|
26
|
+
crsiRuleInjection = true,
|
|
27
|
+
): string {
|
|
20
28
|
let memoryDir: string
|
|
21
29
|
const home = homedir()
|
|
22
30
|
|
|
@@ -56,63 +64,16 @@ function loadAgentMemory(agentName: string, scope: 'user' | 'project' | 'local')
|
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
// Load auto-accumulated experience (always from user scope)
|
|
60
|
-
|
|
61
|
-
const experienceContent = exp.getExperience()
|
|
62
|
-
|
|
67
|
+
// Load auto-accumulated experience rules (always from user scope)
|
|
68
|
+
// Gated by crsi.ruleInjection feature flag
|
|
63
69
|
let experienceMemory = ''
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const failureIdx = lines.findIndex((l) => l.startsWith('## Failure Patterns'))
|
|
69
|
-
|
|
70
|
-
// Header: content before the first section
|
|
71
|
-
const firstSection = Math.min(
|
|
72
|
-
successIdx !== -1 ? successIdx : Infinity,
|
|
73
|
-
failureIdx !== -1 ? failureIdx : Infinity,
|
|
74
|
-
statsIdx !== -1 ? statsIdx : Infinity,
|
|
75
|
-
)
|
|
76
|
-
const header = lines.slice(0, firstSection === Infinity ? 3 : firstSection)
|
|
77
|
-
|
|
78
|
-
// Last 5 success entries (summary line only, not detail)
|
|
79
|
-
const successLines: string[] = []
|
|
80
|
-
if (successIdx !== -1) {
|
|
81
|
-
const endIdx = Math.min(
|
|
82
|
-
failureIdx !== -1 && failureIdx > successIdx ? failureIdx : Infinity,
|
|
83
|
-
statsIdx !== -1 && statsIdx > successIdx ? statsIdx : Infinity,
|
|
84
|
-
)
|
|
85
|
-
successLines.push(
|
|
86
|
-
...lines
|
|
87
|
-
.slice(successIdx + 1, endIdx === Infinity ? undefined : endIdx)
|
|
88
|
-
.filter((l) => l.startsWith('- ['))
|
|
89
|
-
.slice(-5),
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Last 3 failure entries (summary line only, not detail)
|
|
94
|
-
const failureLines: string[] = []
|
|
95
|
-
if (failureIdx !== -1) {
|
|
96
|
-
const endIdx = statsIdx !== -1 && statsIdx > failureIdx ? statsIdx : Infinity
|
|
97
|
-
failureLines.push(
|
|
98
|
-
...lines
|
|
99
|
-
.slice(failureIdx + 1, endIdx === Infinity ? undefined : endIdx)
|
|
100
|
-
.filter((l) => l.startsWith('- ['))
|
|
101
|
-
.slice(-3),
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stats section (header + stat line)
|
|
106
|
-
const stats = statsIdx !== -1 ? lines.slice(statsIdx, statsIdx + 3) : []
|
|
70
|
+
if (crsiRuleInjection) {
|
|
71
|
+
const exp = new AgentExperience(agentName)
|
|
72
|
+
const extractor = new ExperienceRuleExtractor()
|
|
73
|
+
const rules = extractor.prioritize(exp.getRules())
|
|
107
74
|
|
|
108
|
-
if (
|
|
109
|
-
experienceMemory =
|
|
110
|
-
'## Agent Experience',
|
|
111
|
-
...header.filter((l) => l.trim()),
|
|
112
|
-
...successLines,
|
|
113
|
-
...failureLines,
|
|
114
|
-
...stats,
|
|
115
|
-
].join('\n')
|
|
75
|
+
if (rules.length > 0) {
|
|
76
|
+
experienceMemory = extractor.formatForInjection(rules)
|
|
116
77
|
}
|
|
117
78
|
}
|
|
118
79
|
|
|
@@ -139,6 +100,7 @@ export function createAgentContext(
|
|
|
139
100
|
agentDef: AgentDefinition,
|
|
140
101
|
toolRegistry: Map<string, ToolDefinition>,
|
|
141
102
|
contextWindow?: number,
|
|
103
|
+
crsiConfig?: Partial<CrsiConfig>,
|
|
142
104
|
): AgentContextResult {
|
|
143
105
|
// Create isolated context
|
|
144
106
|
const context = new ContextManager({
|
|
@@ -149,7 +111,8 @@ export function createAgentContext(
|
|
|
149
111
|
// Build system prompt with optional agent memory
|
|
150
112
|
let systemPrompt = agentDef.systemPrompt
|
|
151
113
|
if (agentDef.memory) {
|
|
152
|
-
const
|
|
114
|
+
const ruleInjection = crsiConfig?.ruleInjection !== false
|
|
115
|
+
const memory = loadAgentMemory(agentDef.name, agentDef.memory, ruleInjection)
|
|
153
116
|
if (memory) {
|
|
154
117
|
systemPrompt = `${systemPrompt}\n\n---\n\n${memory}`
|
|
155
118
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { ExperienceRuleExtractor, type ExperienceRule } from './experience-rules.js'
|
|
3
4
|
|
|
4
5
|
const MAX_EXPERIENCES = 20
|
|
5
6
|
|
|
@@ -46,6 +47,13 @@ export class AgentExperience {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
getRules(): ExperienceRule[] {
|
|
51
|
+
const content = this.getExperience()
|
|
52
|
+
if (!content) return []
|
|
53
|
+
const extractor = new ExperienceRuleExtractor()
|
|
54
|
+
return extractor.extract(content, this.agentName)
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
private appendToSection(section: string, entry: string, type: 'success' | 'failure'): void {
|
|
50
58
|
mkdirSync(this.expDir, { recursive: true })
|
|
51
59
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface RuleEffectiveness {
|
|
5
|
+
ruleId: string
|
|
6
|
+
appliedCount: number
|
|
7
|
+
successAfterCount: number
|
|
8
|
+
preRuleFailureRate: number
|
|
9
|
+
postRuleFailureRate: number
|
|
10
|
+
status: 'active' | 'degrading' | 'disabled'
|
|
11
|
+
createdAt: string
|
|
12
|
+
lastEvaluatedAt: string
|
|
13
|
+
evaluationHistory: Array<{
|
|
14
|
+
date: string
|
|
15
|
+
appliedCount: number
|
|
16
|
+
failureRate: number
|
|
17
|
+
}>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EVAL_THRESHOLD = 10 // evaluate after 10 applications
|
|
21
|
+
const STORE_FILE = 'effectiveness.json'
|
|
22
|
+
|
|
23
|
+
export class EffectivenessTracker {
|
|
24
|
+
private data: Map<string, RuleEffectiveness>
|
|
25
|
+
private storePath: string
|
|
26
|
+
|
|
27
|
+
constructor(storeDir: string = join(process.env.HOME || '~', '.mipham', 'rule-engine')) {
|
|
28
|
+
this.storePath = join(storeDir, STORE_FILE)
|
|
29
|
+
this.data = new Map()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
recordApplication(ruleId: string, success: boolean): void {
|
|
33
|
+
let eff = this.data.get(ruleId)
|
|
34
|
+
if (!eff) {
|
|
35
|
+
eff = {
|
|
36
|
+
ruleId,
|
|
37
|
+
appliedCount: 0,
|
|
38
|
+
successAfterCount: 0,
|
|
39
|
+
preRuleFailureRate: 1.0,
|
|
40
|
+
postRuleFailureRate: 0,
|
|
41
|
+
status: 'active',
|
|
42
|
+
createdAt: new Date().toISOString().slice(0, 10),
|
|
43
|
+
lastEvaluatedAt: '',
|
|
44
|
+
evaluationHistory: [],
|
|
45
|
+
}
|
|
46
|
+
this.data.set(ruleId, eff)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
eff.appliedCount++
|
|
50
|
+
if (success) eff.successAfterCount++
|
|
51
|
+
|
|
52
|
+
// Calculate current failure rate
|
|
53
|
+
if (eff.appliedCount >= EVAL_THRESHOLD) {
|
|
54
|
+
eff.postRuleFailureRate = 1 - eff.successAfterCount / eff.appliedCount
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Evaluate rule effectiveness and return auto-management actions.
|
|
60
|
+
*
|
|
61
|
+
* @param autoRuleManagement — when false, skip auto-degrade/disable/enable logic
|
|
62
|
+
* but still record evaluation history for manual review. Default true.
|
|
63
|
+
*/
|
|
64
|
+
evaluate(autoRuleManagement = true): {
|
|
65
|
+
upgrades: string[]
|
|
66
|
+
degradations: string[]
|
|
67
|
+
disables: string[]
|
|
68
|
+
} {
|
|
69
|
+
const result = {
|
|
70
|
+
upgrades: [] as string[],
|
|
71
|
+
degradations: [] as string[],
|
|
72
|
+
disables: [] as string[],
|
|
73
|
+
}
|
|
74
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
75
|
+
|
|
76
|
+
for (const [ruleId, eff] of this.data) {
|
|
77
|
+
if (eff.appliedCount < EVAL_THRESHOLD) continue
|
|
78
|
+
|
|
79
|
+
eff.lastEvaluatedAt = now
|
|
80
|
+
eff.evaluationHistory.push({
|
|
81
|
+
date: now,
|
|
82
|
+
appliedCount: eff.appliedCount,
|
|
83
|
+
failureRate: eff.postRuleFailureRate,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Keep max 10 history entries
|
|
87
|
+
if (eff.evaluationHistory.length > 10) {
|
|
88
|
+
eff.evaluationHistory = eff.evaluationHistory.slice(-10)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Auto-management is gated by crsi.autoRuleManagement feature flag
|
|
92
|
+
if (!autoRuleManagement) continue
|
|
93
|
+
|
|
94
|
+
// Degrade: active rule with high failure rate
|
|
95
|
+
if (eff.status === 'active' && eff.postRuleFailureRate > 0.6) {
|
|
96
|
+
eff.status = 'degrading'
|
|
97
|
+
result.degradations.push(ruleId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Disable: degrading rule with no improvement across evaluations
|
|
101
|
+
if (eff.status === 'degrading' && eff.evaluationHistory.length >= 2) {
|
|
102
|
+
const last = eff.evaluationHistory[eff.evaluationHistory.length - 1]
|
|
103
|
+
const prev = eff.evaluationHistory[eff.evaluationHistory.length - 2]
|
|
104
|
+
if (last!.failureRate >= prev!.failureRate) {
|
|
105
|
+
eff.status = 'disabled'
|
|
106
|
+
result.disables.push(ruleId)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Upgrade: degrading rule that has improved significantly
|
|
111
|
+
if (eff.status === 'degrading' && eff.postRuleFailureRate < 0.4) {
|
|
112
|
+
eff.status = 'active'
|
|
113
|
+
result.upgrades.push(ruleId)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getEffectiveness(ruleId: string): RuleEffectiveness | null {
|
|
121
|
+
return this.data.get(ruleId) || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
persist(): void {
|
|
125
|
+
const dir = dirname(this.storePath)
|
|
126
|
+
mkdirSync(dir, { recursive: true })
|
|
127
|
+
|
|
128
|
+
const obj: Record<string, RuleEffectiveness> = {}
|
|
129
|
+
for (const [k, v] of this.data) {
|
|
130
|
+
obj[k] = v
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(this.storePath, JSON.stringify(obj, null, 2), 'utf-8')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
load(): void {
|
|
136
|
+
if (!existsSync(this.storePath)) return
|
|
137
|
+
try {
|
|
138
|
+
const raw = JSON.parse(readFileSync(this.storePath, 'utf-8'))
|
|
139
|
+
this.data = new Map(Object.entries(raw))
|
|
140
|
+
} catch {
|
|
141
|
+
// Corrupt file — start fresh
|
|
142
|
+
this.data = new Map()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get allRules(): RuleEffectiveness[] {
|
|
147
|
+
return Array.from(this.data.values())
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -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,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
|
@@ -22,6 +22,12 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
22
22
|
mcp: { oauthEnabled: true },
|
|
23
23
|
context: { useRealTokenizer: true, adaptiveThresholds: true },
|
|
24
24
|
},
|
|
25
|
+
crsi: {
|
|
26
|
+
ruleInjection: true,
|
|
27
|
+
preToolHook: true,
|
|
28
|
+
autoPatternAnalysis: true,
|
|
29
|
+
autoRuleManagement: true,
|
|
30
|
+
},
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
package/src/core/engine.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ToolDefinition,
|
|
4
4
|
ToolResult,
|
|
5
5
|
InferenceHookConfig,
|
|
6
|
+
CrsiConfig,
|
|
6
7
|
} from '../shared/index.ts'
|
|
7
8
|
import { ProviderRegistry } from '../providers/registry'
|
|
8
9
|
import { ContextManager } from './context'
|
|
@@ -16,6 +17,9 @@ import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
|
16
17
|
import type { SkillsLoader } from '../skills/loader'
|
|
17
18
|
import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
18
19
|
import { RulesLoader } from './rules-loader'
|
|
20
|
+
import { ExperienceRuleEngine } from './rule-engine.js'
|
|
21
|
+
import { PatternAnalyzer } from '../agent/pattern-analyzer.js'
|
|
22
|
+
import { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
|
|
19
23
|
import { UsageTracker } from './usage-tracker'
|
|
20
24
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
21
25
|
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
@@ -37,6 +41,9 @@ export class QueryEngine {
|
|
|
37
41
|
private agentRegistry?: AgentRegistry
|
|
38
42
|
private agentViewManager?: AgentViewManager
|
|
39
43
|
private skillsLoader?: SkillsLoader
|
|
44
|
+
private ruleEngine?: ExperienceRuleEngine
|
|
45
|
+
private _patternAnalyzer?: PatternAnalyzer
|
|
46
|
+
private _effectivenessTracker?: EffectivenessTracker
|
|
40
47
|
private goal?: string
|
|
41
48
|
private maxGoalLoops = 20
|
|
42
49
|
private lastAssistantContent?: string
|
|
@@ -54,7 +61,10 @@ export class QueryEngine {
|
|
|
54
61
|
private context: ContextManager,
|
|
55
62
|
private tools: Map<string, ToolDefinition>,
|
|
56
63
|
private permission: PermissionSystem = new PermissionSystem('default'),
|
|
57
|
-
|
|
64
|
+
ruleEngine?: ExperienceRuleEngine,
|
|
65
|
+
) {
|
|
66
|
+
this.ruleEngine = ruleEngine
|
|
67
|
+
}
|
|
58
68
|
|
|
59
69
|
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
60
70
|
private sessionId: string = 'session-1'
|
|
@@ -128,6 +138,8 @@ export class QueryEngine {
|
|
|
128
138
|
private usageTracker = new UsageTracker()
|
|
129
139
|
/** Inference hook (DLP) configuration. */
|
|
130
140
|
private inferenceHookConfig?: InferenceHookConfig
|
|
141
|
+
/** CRSI feature flags. */
|
|
142
|
+
private crsiConfig?: Partial<CrsiConfig>
|
|
131
143
|
|
|
132
144
|
/** Register the rules loader. */
|
|
133
145
|
setRulesLoader(loader: RulesLoader): void {
|
|
@@ -140,6 +152,11 @@ export class QueryEngine {
|
|
|
140
152
|
this.inferenceHookConfig = config
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/** Register CRSI feature flags. */
|
|
156
|
+
setCrsiConfig(config: Partial<CrsiConfig> | undefined): void {
|
|
157
|
+
this.crsiConfig = config
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
/** Pending task notifications from background agents (cleared after draining). */
|
|
144
161
|
private pendingTaskNotifications: Array<StreamChunk> = []
|
|
145
162
|
|
|
@@ -796,6 +813,7 @@ export class QueryEngine {
|
|
|
796
813
|
|
|
797
814
|
// Run PreToolUse hooks
|
|
798
815
|
let effectiveParams = params
|
|
816
|
+
let hookWarnings: string[] = []
|
|
799
817
|
if (this.hookEngine) {
|
|
800
818
|
const preResult = await this.hookEngine.executePreToolUse(name, params, this.sessionId)
|
|
801
819
|
if (!preResult.allowed) {
|
|
@@ -810,6 +828,17 @@ export class QueryEngine {
|
|
|
810
828
|
}
|
|
811
829
|
}
|
|
812
830
|
|
|
831
|
+
// CRSI RuleEngine intercept — gated by crsi.preToolHook feature flag
|
|
832
|
+
if (this.crsiConfig?.preToolHook !== false && this.ruleEngine) {
|
|
833
|
+
const ruleResult = this.ruleEngine.intercept(name, effectiveParams)
|
|
834
|
+
if (Object.keys(ruleResult.modified).length > 0) {
|
|
835
|
+
effectiveParams = ruleResult.modified
|
|
836
|
+
}
|
|
837
|
+
if (ruleResult.warnings.length > 0) {
|
|
838
|
+
hookWarnings = ruleResult.warnings
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
813
842
|
try {
|
|
814
843
|
const result = await tool.execute(effectiveParams, {
|
|
815
844
|
cwd: process.cwd(),
|
|
@@ -823,16 +852,35 @@ export class QueryEngine {
|
|
|
823
852
|
agentRegistry: this.agentRegistry,
|
|
824
853
|
backgroundAgentRegistry: getBackgroundAgentRegistry(),
|
|
825
854
|
permissionSystem: this.permission,
|
|
855
|
+
ruleEngine: this.ruleEngine,
|
|
826
856
|
})
|
|
827
857
|
|
|
828
858
|
// Track touched files for rules matching
|
|
829
859
|
this.trackTouchedFile(name, effectiveParams)
|
|
830
860
|
|
|
861
|
+
// Prepend CRSI warnings to result content
|
|
862
|
+
if (hookWarnings.length > 0 && result.success) {
|
|
863
|
+
result.content = hookWarnings.join('\n') + '\n' + (result.content || '')
|
|
864
|
+
}
|
|
865
|
+
|
|
831
866
|
// Run PostToolUse hooks
|
|
832
867
|
if (this.hookEngine) {
|
|
833
868
|
await this.hookEngine.executePostToolUse(name, effectiveParams, result, this.sessionId)
|
|
834
869
|
}
|
|
835
870
|
|
|
871
|
+
// CRSI: Track rule effectiveness after tool execution
|
|
872
|
+
if (hookWarnings.length > 0) {
|
|
873
|
+
const tracker = this.getEffectivenessTracker()
|
|
874
|
+
for (const warning of hookWarnings) {
|
|
875
|
+
const match = warning.match(/^\[rule:([^\]]+)\]/)
|
|
876
|
+
const ruleId = match?.[1]
|
|
877
|
+
if (ruleId) {
|
|
878
|
+
tracker.recordApplication(ruleId, result.success)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
tracker.persist()
|
|
882
|
+
}
|
|
883
|
+
|
|
836
884
|
return result
|
|
837
885
|
} catch (err) {
|
|
838
886
|
// Sanitize error: strip stack traces and internal paths to prevent
|
|
@@ -864,6 +912,27 @@ export class QueryEngine {
|
|
|
864
912
|
return this.tools
|
|
865
913
|
}
|
|
866
914
|
|
|
915
|
+
getRuleEngine(): ExperienceRuleEngine | undefined {
|
|
916
|
+
return this.ruleEngine
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Lazily-initialized PatternAnalyzer singleton. */
|
|
920
|
+
getPatternAnalyzer(): PatternAnalyzer {
|
|
921
|
+
if (!this._patternAnalyzer) {
|
|
922
|
+
this._patternAnalyzer = new PatternAnalyzer()
|
|
923
|
+
}
|
|
924
|
+
return this._patternAnalyzer
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Lazily-initialized EffectivenessTracker singleton. */
|
|
928
|
+
getEffectivenessTracker(): EffectivenessTracker {
|
|
929
|
+
if (!this._effectivenessTracker) {
|
|
930
|
+
this._effectivenessTracker = new EffectivenessTracker()
|
|
931
|
+
this._effectivenessTracker.load()
|
|
932
|
+
}
|
|
933
|
+
return this._effectivenessTracker
|
|
934
|
+
}
|
|
935
|
+
|
|
867
936
|
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
868
937
|
registerTool(tool: ToolDefinition): void {
|
|
869
938
|
if (this.tools.has(tool.name)) {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ExperienceRule } from '../agent/experience-rules.js'
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join, dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export interface ToolRule {
|
|
6
|
+
id: string
|
|
7
|
+
toolName: string
|
|
8
|
+
category: string
|
|
9
|
+
match: (params: Record<string, unknown>) => boolean
|
|
10
|
+
fix: (params: Record<string, unknown>) => {
|
|
11
|
+
modified: Record<string, unknown>
|
|
12
|
+
warning: string
|
|
13
|
+
}
|
|
14
|
+
source: 'builtin' | 'pattern-analyzer' | 'manual'
|
|
15
|
+
enabled: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BUILTIN_RULES: ToolRule[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'rule-timeout-bash-heavy',
|
|
21
|
+
toolName: 'Bash',
|
|
22
|
+
category: 'timeout',
|
|
23
|
+
match: (p: Record<string, unknown>) => {
|
|
24
|
+
const cmd = String(p.command ?? '')
|
|
25
|
+
const heavy = /npm (install|ci|test)|docker build|pnpm install|cargo build|brew install/.test(
|
|
26
|
+
cmd,
|
|
27
|
+
)
|
|
28
|
+
if (!heavy) return false
|
|
29
|
+
const timeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
30
|
+
return !timeout || timeout < 300_000
|
|
31
|
+
},
|
|
32
|
+
fix: (p: Record<string, unknown>) => {
|
|
33
|
+
const prevTimeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
34
|
+
return {
|
|
35
|
+
modified: { ...p, timeout: 300_000 },
|
|
36
|
+
warning: `⏱️ timeout 已从 ${prevTimeout || 'default'}ms 自动提升至 300000ms(该命令类型历史超时率 > 50%)`,
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
source: 'builtin',
|
|
40
|
+
enabled: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'rule-git-force-protection',
|
|
44
|
+
toolName: 'Bash',
|
|
45
|
+
category: 'tool-params',
|
|
46
|
+
match: (p: Record<string, unknown>) => {
|
|
47
|
+
const cmd = String(p.command ?? '')
|
|
48
|
+
return /git (push|reset) .*--force/.test(cmd) && !p.dangerouslyDisableSandbox
|
|
49
|
+
},
|
|
50
|
+
fix: (p: Record<string, unknown>) => ({
|
|
51
|
+
modified: p,
|
|
52
|
+
warning: '⚠️ 检测到 git --force 操作。如需执行请设置 dangerouslyDisableSandbox: true',
|
|
53
|
+
}),
|
|
54
|
+
source: 'builtin',
|
|
55
|
+
enabled: true,
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export class ExperienceRuleEngine {
|
|
60
|
+
private rules: ToolRule[]
|
|
61
|
+
private storePath: string
|
|
62
|
+
|
|
63
|
+
constructor(storeDir: string = join(process.env.HOME || '~', '.mipham', 'rule-engine')) {
|
|
64
|
+
this.rules = [...BUILTIN_RULES.map((r) => ({ ...r }))]
|
|
65
|
+
this.storePath = join(storeDir, 'rules.json')
|
|
66
|
+
this.load()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
register(rule: ToolRule): void {
|
|
70
|
+
// Replace if same ID exists, otherwise append
|
|
71
|
+
const idx = this.rules.findIndex((r) => r.id === rule.id)
|
|
72
|
+
if (idx !== -1) {
|
|
73
|
+
this.rules[idx] = rule
|
|
74
|
+
} else {
|
|
75
|
+
this.rules.push(rule)
|
|
76
|
+
}
|
|
77
|
+
this.persist()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
intercept(
|
|
81
|
+
toolName: string,
|
|
82
|
+
params: Record<string, unknown>,
|
|
83
|
+
): { modified: Record<string, unknown>; warnings: string[] } {
|
|
84
|
+
let modified = params
|
|
85
|
+
const warnings: string[] = []
|
|
86
|
+
|
|
87
|
+
for (const rule of this.rules) {
|
|
88
|
+
if (!rule.enabled) continue
|
|
89
|
+
if (rule.toolName !== toolName) continue
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (rule.match(modified)) {
|
|
93
|
+
const result = rule.fix(modified)
|
|
94
|
+
modified = result.modified
|
|
95
|
+
if (result.warning) {
|
|
96
|
+
warnings.push(`[rule:${rule.id}] ${result.warning}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Rule match/fix failures never block execution
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { modified, warnings }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
convertFromExperienceRules(experienceRules: ExperienceRule[]): ToolRule[] {
|
|
108
|
+
return experienceRules.map((er): ToolRule => {
|
|
109
|
+
// Map experience rule category to tool name
|
|
110
|
+
const toolNameMap: Record<string, string> = {
|
|
111
|
+
timeout: 'Bash',
|
|
112
|
+
'tool-params': 'Bash',
|
|
113
|
+
import: 'Write',
|
|
114
|
+
search: 'Grep',
|
|
115
|
+
semantic: 'Bash',
|
|
116
|
+
}
|
|
117
|
+
const toolName = toolNameMap[er.category] || 'Bash'
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: er.id,
|
|
121
|
+
toolName,
|
|
122
|
+
category: er.category,
|
|
123
|
+
match: (p: Record<string, unknown>): boolean => {
|
|
124
|
+
if (er.category === 'timeout') {
|
|
125
|
+
const cmd = String(p.command ?? '')
|
|
126
|
+
const heavy = /npm|docker|pnpm|cargo|brew|install|build/.test(cmd)
|
|
127
|
+
if (!heavy) return false
|
|
128
|
+
const timeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
129
|
+
return !timeout || timeout < 300_000
|
|
130
|
+
}
|
|
131
|
+
// Default: always match for the given tool
|
|
132
|
+
return true
|
|
133
|
+
},
|
|
134
|
+
fix: (p: Record<string, unknown>) => ({
|
|
135
|
+
modified: { ...p, timeout: 300_000 },
|
|
136
|
+
warning: `⏱️ [auto-rule] ${er.action} (${er.evidence.failureCount} 次历史失败)`,
|
|
137
|
+
}),
|
|
138
|
+
source: 'pattern-analyzer',
|
|
139
|
+
enabled: true,
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getActiveRules(): ToolRule[] {
|
|
145
|
+
return this.rules.filter((r) => r.enabled)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setRuleEnabled(id: string, enabled: boolean): void {
|
|
149
|
+
const rule = this.rules.find((r) => r.id === id)
|
|
150
|
+
if (rule) {
|
|
151
|
+
rule.enabled = enabled
|
|
152
|
+
this.persist()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Persist non-builtin rules to disk. */
|
|
157
|
+
persist(): void {
|
|
158
|
+
const nonBuiltin = this.rules.filter((r) => r.source !== 'builtin')
|
|
159
|
+
const dir = dirname(this.storePath)
|
|
160
|
+
mkdirSync(dir, { recursive: true })
|
|
161
|
+
writeFileSync(this.storePath, JSON.stringify(nonBuiltin, null, 2), 'utf-8')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Load persisted non-builtin rules from disk. Rejects rules whose IDs conflict with builtins. */
|
|
165
|
+
load(): void {
|
|
166
|
+
if (!existsSync(this.storePath)) return
|
|
167
|
+
try {
|
|
168
|
+
const raw = JSON.parse(readFileSync(this.storePath, 'utf-8')) as ToolRule[]
|
|
169
|
+
const builtinIds = new Set(BUILTIN_RULES.map((r) => r.id))
|
|
170
|
+
for (const rule of raw) {
|
|
171
|
+
// Reject if a builtin with the same ID exists (builtins always win)
|
|
172
|
+
if (builtinIds.has(rule.id)) continue
|
|
173
|
+
this.rules.push(rule)
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Corrupt file — start fresh with only builtins
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { InstructionsLoader } from './core/instructions'
|
|
|
20
20
|
import { loadSessionMemories, getMemoryManager } from './core/memory/memory-loader'
|
|
21
21
|
import { ContextManager } from './core/context'
|
|
22
22
|
import { QueryEngine } from './core/engine'
|
|
23
|
+
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
23
24
|
import { SessionStore } from './core/session-store'
|
|
24
25
|
import type { PermissionLevel } from './shared/types'
|
|
25
26
|
import { SkillsLoader } from './skills/loader'
|
|
@@ -247,7 +248,8 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
247
248
|
const artifactServer = new ArtifactServer(artifactsDir, ARTIFACT_PORT)
|
|
248
249
|
|
|
249
250
|
// Create query engine
|
|
250
|
-
const
|
|
251
|
+
const ruleEngine = new ExperienceRuleEngine()
|
|
252
|
+
const engine = new QueryEngine(registry, context, tools, undefined, ruleEngine)
|
|
251
253
|
engine.setHookEngine(hookEngine)
|
|
252
254
|
engine.setArtifactServer(artifactServer)
|
|
253
255
|
engine.setAgentViewManager(agentViewManager)
|
package/src/shared/types.ts
CHANGED
|
@@ -72,6 +72,7 @@ export interface ToolContext {
|
|
|
72
72
|
agentRegistry?: import('../agent/agent-registry').AgentRegistry
|
|
73
73
|
backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
|
|
74
74
|
permissionSystem?: import('../core/permission').PermissionSystem
|
|
75
|
+
ruleEngine?: import('../core/rule-engine').ExperienceRuleEngine
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// ── Artifact Types ──
|
|
@@ -162,6 +163,8 @@ export interface MiphamConfig {
|
|
|
162
163
|
}
|
|
163
164
|
/** Phase 9 feature flags. All default to true. */
|
|
164
165
|
features?: Partial<FeatureFlags>
|
|
166
|
+
/** Phase 10 CRSI feature flags. All default to true. */
|
|
167
|
+
crsi?: Partial<CrsiConfig>
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
export interface FeatureFlags {
|
|
@@ -169,6 +172,17 @@ export interface FeatureFlags {
|
|
|
169
172
|
context: { useRealTokenizer: boolean; adaptiveThresholds: boolean }
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
export interface CrsiConfig {
|
|
176
|
+
/** Extract and inject experience rules into agent system prompts. */
|
|
177
|
+
ruleInjection: boolean
|
|
178
|
+
/** Intercept tool calls and auto-fix known failure patterns before execution. */
|
|
179
|
+
preToolHook: boolean
|
|
180
|
+
/** Analyze agent outcomes for recurring failure patterns. */
|
|
181
|
+
autoPatternAnalysis: boolean
|
|
182
|
+
/** Auto-degrade/disable low-effectiveness rules based on success-rate tracking. */
|
|
183
|
+
autoRuleManagement: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
export interface McpServerConfig {
|
|
173
187
|
name: string
|
|
174
188
|
command: string
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -60,7 +60,13 @@ export const agentTool: ToolDefinition = {
|
|
|
60
60
|
const agentDef = ctx.agentRegistry?.resolve(agentType)
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
|
-
const sub = new SubAgent(
|
|
63
|
+
const sub = new SubAgent(
|
|
64
|
+
registry,
|
|
65
|
+
toolRegistry,
|
|
66
|
+
ctx.permissionSystem,
|
|
67
|
+
undefined,
|
|
68
|
+
ctx.ruleEngine,
|
|
69
|
+
)
|
|
64
70
|
const result = await sub.execute(prompt, description, {
|
|
65
71
|
type: agentType,
|
|
66
72
|
agentDef,
|
package/src/ui/commands.ts
CHANGED
|
@@ -446,6 +446,124 @@ const skillsCmd: CommandHandler = (ctx) => {
|
|
|
446
446
|
return { content: lines.join('\n') }
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
// ═══════════════════════════════════════════════════════════════
|
|
450
|
+
// CRSI (Conversational Rule Self-Improvement)
|
|
451
|
+
// ═══════════════════════════════════════════════════════════════
|
|
452
|
+
|
|
453
|
+
const crsiRulesCmd: CommandHandler = (ctx) => {
|
|
454
|
+
const engine = ctx.engine.getRuleEngine()
|
|
455
|
+
if (!engine) {
|
|
456
|
+
return { content: 'CRSI rule engine is not available.' }
|
|
457
|
+
}
|
|
458
|
+
const rules = engine.getActiveRules()
|
|
459
|
+
if (rules.length === 0) {
|
|
460
|
+
return { content: 'No active CRSI rules.' }
|
|
461
|
+
}
|
|
462
|
+
const lines: string[] = ['## Active CRSI Rules', '']
|
|
463
|
+
for (const r of rules) {
|
|
464
|
+
const status = r.enabled ? '✅' : '⛔'
|
|
465
|
+
lines.push(`- \`${r.id}\` [${r.category}] ${r.toolName} — ${r.source} ${status}`)
|
|
466
|
+
}
|
|
467
|
+
lines.push('', `Total: ${rules.length} active rules`)
|
|
468
|
+
lines.push('', 'Use `/crsi disable <rule-id>` to disable a rule.')
|
|
469
|
+
return { content: lines.join('\n') }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const crsiDisableCmd: CommandHandler = (ctx, args) => {
|
|
473
|
+
const engine = ctx.engine.getRuleEngine()
|
|
474
|
+
if (!engine) {
|
|
475
|
+
return { content: 'CRSI rule engine is not available.' }
|
|
476
|
+
}
|
|
477
|
+
const ruleId = args[0]?.trim()
|
|
478
|
+
if (!ruleId) {
|
|
479
|
+
return { content: 'Usage: /crsi disable <rule-id>' }
|
|
480
|
+
}
|
|
481
|
+
engine.setRuleEnabled(ruleId, false)
|
|
482
|
+
return {
|
|
483
|
+
content: `Rule \`${ruleId}\` has been disabled. Use \`/crsi restore ${ruleId}\` to re-enable.`,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const crsiAnalyzeCmd: CommandHandler = async (ctx) => {
|
|
488
|
+
const analyzer = ctx.engine.getPatternAnalyzer()
|
|
489
|
+
const engine = ctx.engine.getRuleEngine()
|
|
490
|
+
if (!engine) {
|
|
491
|
+
return { content: 'CRSI system is not available.' }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const patterns = analyzer.analyzeAllAgents()
|
|
495
|
+
if (patterns.length === 0) {
|
|
496
|
+
return { content: 'No failure patterns found across agents.' }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let registered = 0
|
|
500
|
+
for (const pattern of patterns) {
|
|
501
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
502
|
+
engine.register(toolRule)
|
|
503
|
+
registered++
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const lines: string[] = [
|
|
507
|
+
'## CRSI Analysis Complete',
|
|
508
|
+
'',
|
|
509
|
+
`Found ${patterns.length} patterns, ${registered} rules registered.`,
|
|
510
|
+
'',
|
|
511
|
+
]
|
|
512
|
+
for (const p of patterns) {
|
|
513
|
+
lines.push(
|
|
514
|
+
`- [${p.category}] \`${p.agentName}\` — ${p.frequency} failures (${p.confidence} confidence)`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
return { content: lines.join('\n') }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const crsiRestoreCmd: CommandHandler = (ctx, args) => {
|
|
521
|
+
const engine = ctx.engine.getRuleEngine()
|
|
522
|
+
if (!engine) {
|
|
523
|
+
return { content: 'CRSI rule engine is not available.' }
|
|
524
|
+
}
|
|
525
|
+
const ruleId = args.join(' ').trim()
|
|
526
|
+
if (!ruleId) {
|
|
527
|
+
return { content: 'Usage: /crsi restore <rule-id>' }
|
|
528
|
+
}
|
|
529
|
+
engine.setRuleEnabled(ruleId, true)
|
|
530
|
+
return { content: `Rule \`${ruleId}\` has been re-enabled.` }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const crsiStatsCmd: CommandHandler = async (ctx) => {
|
|
534
|
+
const engine = ctx.engine.getRuleEngine()
|
|
535
|
+
const tracker = ctx.engine.getEffectivenessTracker()
|
|
536
|
+
if (!engine) {
|
|
537
|
+
return { content: 'CRSI rule engine is not available.' }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const rules = engine.getActiveRules()
|
|
541
|
+
const lines: string[] = ['## CRSI Statistics', '']
|
|
542
|
+
lines.push(`Total active rules: ${rules.length}`)
|
|
543
|
+
lines.push(`Builtin: ${rules.filter((r) => r.source === 'builtin').length}`)
|
|
544
|
+
lines.push(`Auto-generated: ${rules.filter((r) => r.source === 'pattern-analyzer').length}`)
|
|
545
|
+
lines.push(`Manual: ${rules.filter((r) => r.source === 'manual').length}`)
|
|
546
|
+
|
|
547
|
+
if (tracker) {
|
|
548
|
+
let totalInterceptions = 0
|
|
549
|
+
let totalSuccesses = 0
|
|
550
|
+
for (const r of rules) {
|
|
551
|
+
const eff = tracker.getEffectiveness(r.id)
|
|
552
|
+
if (eff) {
|
|
553
|
+
totalInterceptions += eff.appliedCount
|
|
554
|
+
totalSuccesses += eff.successAfterCount
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
lines.push('')
|
|
558
|
+
lines.push(`Total interceptions: ${totalInterceptions}`)
|
|
559
|
+
lines.push(
|
|
560
|
+
`Success rate after rules: ${totalInterceptions > 0 ? Math.round((totalSuccesses / totalInterceptions) * 100) : 0}%`,
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return { content: lines.join('\n') }
|
|
565
|
+
}
|
|
566
|
+
|
|
449
567
|
// ═══════════════════════════════════════════════════════════════
|
|
450
568
|
// Workflow
|
|
451
569
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3072,6 +3190,11 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
3072
3190
|
'/plugin-enable': 'Plugins',
|
|
3073
3191
|
'/plugin-disable': 'Plugins',
|
|
3074
3192
|
'/commands': 'Tools & Skills',
|
|
3193
|
+
'/crsi rules': 'Tools & Skills',
|
|
3194
|
+
'/crsi disable': 'Tools & Skills',
|
|
3195
|
+
'/crsi analyze': 'Tools & Skills',
|
|
3196
|
+
'/crsi restore': 'Tools & Skills',
|
|
3197
|
+
'/crsi stats': 'Tools & Skills',
|
|
3075
3198
|
'/plan': 'Workflow',
|
|
3076
3199
|
'/no-plan': 'Workflow',
|
|
3077
3200
|
'/tdd': 'Workflow',
|
|
@@ -3209,6 +3332,11 @@ registry.set('/remove-plugin', removePluginCmd)
|
|
|
3209
3332
|
registry.set('/plugin-enable', pluginEnableCmd)
|
|
3210
3333
|
registry.set('/plugin-disable', pluginDisableCmd)
|
|
3211
3334
|
registry.set('/commands', commandsListCmd)
|
|
3335
|
+
registry.set('/crsi rules', crsiRulesCmd)
|
|
3336
|
+
registry.set('/crsi disable', crsiDisableCmd)
|
|
3337
|
+
registry.set('/crsi analyze', crsiAnalyzeCmd)
|
|
3338
|
+
registry.set('/crsi restore', crsiRestoreCmd)
|
|
3339
|
+
registry.set('/crsi stats', crsiStatsCmd)
|
|
3212
3340
|
|
|
3213
3341
|
// Workflow
|
|
3214
3342
|
registry.set('/plan', planCmd)
|
|
@@ -3359,6 +3487,11 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
3359
3487
|
'/remove-plugin': 'Remove an installed plugin',
|
|
3360
3488
|
'/plugin-enable': 'Enable a disabled plugin',
|
|
3361
3489
|
'/plugin-disable': 'Disable an enabled plugin',
|
|
3490
|
+
'/crsi rules': 'List all active CRSI rules with their status',
|
|
3491
|
+
'/crsi disable': 'Disable a CRSI rule by ID',
|
|
3492
|
+
'/crsi analyze': 'Manually trigger CRSI pattern analysis across all agents',
|
|
3493
|
+
'/crsi restore': 'Restore a disabled or degraded CRSI rule',
|
|
3494
|
+
'/crsi stats': 'Show CRSI overall effectiveness statistics',
|
|
3362
3495
|
'/plan': 'Enter plan mode',
|
|
3363
3496
|
'/no-plan': 'Exit plan mode',
|
|
3364
3497
|
'/tdd': 'Test-Driven Development workflow (RED → GREEN → REFACTOR)',
|