@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -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(agentName: string, scope: 'user' | 'project' | 'local'): string {
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
- const exp = new AgentExperience(agentName)
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 (experienceContent) {
65
- const lines = experienceContent.split('\n')
66
- const statsIdx = lines.findIndex((l) => l.startsWith('## Stats'))
67
- const successIdx = lines.findIndex((l) => l.startsWith('## Success Patterns'))
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 (successLines.length > 0 || failureLines.length > 0) {
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 memory = loadAgentMemory(agentDef.name, agentDef.memory)
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
+ }
@@ -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
  }
@@ -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
  }
@@ -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 = {
@@ -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 engine = new QueryEngine(registry, context, tools)
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)
@@ -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
@@ -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(registry, toolRegistry, ctx.permissionSystem)
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,
@@ -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)',