@miphamai/cli 0.33.1 → 0.34.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.
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Emotion Detector — User Frustration Detection
3
+ *
4
+ * Inspired by Claude Code's emotion detection: parses user input for signals
5
+ * of frustration, confusion, or satisfaction. Adjusts agent behavior accordingly:
6
+ * - Frustration → be more concise, skip explanations, get to the fix faster
7
+ * - Confusion → explain more, offer context, slow down
8
+ * - Satisfaction → maintain current mode
9
+ *
10
+ * Uses regex heuristics (no LLM call) for zero-latency detection.
11
+ * Designed as a lightweight pre-processor in the input pipeline.
12
+ */
13
+
14
+ // ── Types ──
15
+
16
+ export type Emotion =
17
+ | 'frustrated' // User is angry/annoyed — move fast, skip explanations
18
+ | 'confused' // User doesn't understand — explain more
19
+ | 'impatient' // User wants speed — be terse
20
+ | 'satisfied' // User is happy — continue normally
21
+ | 'neutral' // No strong signal
22
+
23
+ export interface EmotionResult {
24
+ emotion: Emotion
25
+ /** 0.0-1.0 confidence in the detected emotion */
26
+ confidence: number
27
+ /** Specific phrases that triggered the detection */
28
+ triggers: string[]
29
+ /** Suggested behavior adjustment for the agent */
30
+ suggestion: string
31
+ }
32
+
33
+ // ── Constants ──
34
+
35
+ /** Patterns that indicate frustration/anger. Ordered by severity. */
36
+ const FRUSTRATION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
37
+ { pattern: /\b(wtf|wtaf)\b/i, weight: 0.9, label: 'profanity' },
38
+ {
39
+ pattern: /\b(useless|worthless|garbage|rubbish|broken)\b/i,
40
+ weight: 0.8,
41
+ label: 'strong-negative',
42
+ },
43
+ { pattern: /\b(damn|shit|crap)\b/i, weight: 0.7, label: 'mild-profanity' },
44
+ {
45
+ pattern: /\b(stop|don't|do not)\s+(explain|talk|lecture|ramble)\b/i,
46
+ weight: 0.85,
47
+ label: 'stop-explaining',
48
+ },
49
+ {
50
+ pattern: /\b(just\s+fix\s+it|just\s+do\s+it|get\s+on\s+with\s+it)\b/i,
51
+ weight: 0.8,
52
+ label: 'just-fix-it',
53
+ },
54
+ {
55
+ pattern: /\b(no|wrong|incorrect|bad|terrible|awful)\s*[!!]{1,3}/i,
56
+ weight: 0.75,
57
+ label: 'emphatic-no',
58
+ },
59
+ { pattern: /\b(again\?|still|not\s+again)\b/i, weight: 0.7, label: 'repeated-failure' },
60
+ { pattern: /\b(i\s+said|i\s+told\s+you|i\s+already)\b/i, weight: 0.75, label: 'i-already-said' },
61
+ { pattern: /\b(no\s+no\s+no|nope\s+nope)\b/i, weight: 0.7, label: 'repeated-no' },
62
+ { pattern: /[!!]{2,}/, weight: 0.6, label: 'multiple-exclamation' },
63
+ {
64
+ pattern: /\b(why|why\s+would\s+you|what\s+are\s+you\s+doing)\b/i,
65
+ weight: 0.65,
66
+ label: 'why-question',
67
+ },
68
+ { pattern: /[??]{2,}/, weight: 0.4, label: 'multiple-question-marks' },
69
+ ]
70
+
71
+ /** Patterns that indicate confusion / need for more explanation */
72
+ const CONFUSION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
73
+ {
74
+ pattern: /\b(i\s+don'?t\s+understand|i\s+am\s+confused|i'?m\s+confused)\b/i,
75
+ weight: 0.9,
76
+ label: 'explicit-confusion',
77
+ },
78
+ {
79
+ pattern: /\b(what\s+does|what\s+is|what\s+are|how\s+does)\b/i,
80
+ weight: 0.6,
81
+ label: 'what-is-question',
82
+ },
83
+ {
84
+ pattern: /\b(can\s+you\s+explain|explain\s+this|elaborate)\b/i,
85
+ weight: 0.7,
86
+ label: 'ask-for-explanation',
87
+ },
88
+ { pattern: /\b(wait|huh|eh\?+|hmm+)\b/i, weight: 0.5, label: 'hesitation' },
89
+ {
90
+ pattern: /\b(i\s+thought|i\s+expected|shouldn'?t\s+it)\b/i,
91
+ weight: 0.6,
92
+ label: 'mismatch-expectation',
93
+ },
94
+ ]
95
+
96
+ /** Patterns that indicate impatience */
97
+ const IMPATIENCE_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
98
+ { pattern: /\b(hurry|quick|fast|faster|asap|urgent)\b/i, weight: 0.8, label: 'speed-words' },
99
+ {
100
+ pattern: /\b(just\s+the\s+(code|answer|result|fix|summary))\b/i,
101
+ weight: 0.75,
102
+ label: 'just-the-result',
103
+ },
104
+ {
105
+ pattern: /\b(skip\s+(the\s+)?(explanation|details|context|background))\b/i,
106
+ weight: 0.8,
107
+ label: 'skip-explanation',
108
+ },
109
+ { pattern: /\b(tldr|tl;dr|summary|summarize)\b/i, weight: 0.7, label: 'tldr' },
110
+ {
111
+ pattern: /\b(get\s+to\s+the\s+point|cut\s+to\s+the\s+chase)\b/i,
112
+ weight: 0.75,
113
+ label: 'get-to-point',
114
+ },
115
+ ]
116
+
117
+ /** Patterns that indicate satisfaction */
118
+ const SATISFACTION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
119
+ { pattern: /\b(thanks|thank\s+you|thx|ty|tyvm)\b/i, weight: 0.5, label: 'thanks' },
120
+ {
121
+ pattern: /\b(great|awesome|perfect|excellent|amazing|brilliant|beautiful)\b/i,
122
+ weight: 0.6,
123
+ label: 'positive-words',
124
+ },
125
+ { pattern: /\b(nice|good\s+job|well\s+done|love\s+it)\b/i, weight: 0.7, label: 'praise' },
126
+ {
127
+ pattern: /\b(exactly|finally|yes!|that'?s\s+it)\b/i,
128
+ weight: 0.6,
129
+ label: 'confirmation-positive',
130
+ },
131
+ { pattern: /[👍🙌🎉✨💯🔥✅]/u, weight: 0.5, label: 'positive-emoji' },
132
+ ]
133
+
134
+ // ── Detector ──
135
+
136
+ export class EmotionDetector {
137
+ /**
138
+ * Analyze user input for emotional signals.
139
+ *
140
+ * @param input — raw user message text
141
+ * @param recentContext — optional recent conversation turns for pattern detection
142
+ * @returns EmotionResult with detected emotion and behavioral suggestion
143
+ */
144
+ detect(input: string, recentContext?: string[]): EmotionResult {
145
+ // Check frustration first (most actionable)
146
+ const frustration = this.scorePatterns(input, FRUSTRATION_PATTERNS, recentContext)
147
+ if (frustration.score >= 0.5) {
148
+ return {
149
+ emotion: 'frustrated',
150
+ confidence: frustration.score,
151
+ triggers: frustration.triggers,
152
+ suggestion:
153
+ frustration.score >= 0.8
154
+ ? 'User is very frustrated. Be extremely concise. Skip ALL explanations. Output only the fix/result. No preamble, no summary, no "here\'s what I did". One sentence max before code.'
155
+ : 'User is frustrated. Be concise. Skip explanations. Get to the fix immediately.',
156
+ }
157
+ }
158
+
159
+ // Check impatience
160
+ const impatience = this.scorePatterns(input, IMPATIENCE_PATTERNS)
161
+ if (impatience.score >= 0.5) {
162
+ return {
163
+ emotion: 'impatient',
164
+ confidence: impatience.score,
165
+ triggers: impatience.triggers,
166
+ suggestion:
167
+ 'User wants speed. Skip explanations. Output the result immediately. No "let me explain why" — just the answer.',
168
+ }
169
+ }
170
+
171
+ // Check confusion
172
+ const confusion = this.scorePatterns(input, CONFUSION_PATTERNS)
173
+ if (confusion.score >= 0.5) {
174
+ return {
175
+ emotion: 'confused',
176
+ confidence: confusion.score,
177
+ triggers: confusion.triggers,
178
+ suggestion:
179
+ 'User is confused. Explain more. Offer context. Break down complex steps. Ask clarifying questions rather than assuming.',
180
+ }
181
+ }
182
+
183
+ // Check satisfaction
184
+ const satisfaction = this.scorePatterns(input, SATISFACTION_PATTERNS)
185
+ if (satisfaction.score >= 0.5) {
186
+ return {
187
+ emotion: 'satisfied',
188
+ confidence: satisfaction.score,
189
+ triggers: satisfaction.triggers,
190
+ suggestion:
191
+ 'User is satisfied. Maintain current tone and pace. Good time for optional suggestions.',
192
+ }
193
+ }
194
+
195
+ return {
196
+ emotion: 'neutral',
197
+ confidence: 0,
198
+ triggers: [],
199
+ suggestion: 'No strong emotional signal. Proceed normally.',
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Quick check — does this input contain ANY frustration signal?
205
+ * Use as a pre-filter before the full detect() call.
206
+ */
207
+ isFrustrated(input: string): boolean {
208
+ return FRUSTRATION_PATTERNS.some((p) => p.pattern.test(input))
209
+ }
210
+
211
+ /**
212
+ * Detect repeated frustration across multiple turns (escalation detection).
213
+ */
214
+ detectEscalation(recentInputs: string[]): boolean {
215
+ if (recentInputs.length < 2) return false
216
+ const recent = recentInputs.slice(-3)
217
+ const frustrationCount = recent.filter((i) => this.isFrustrated(i)).length
218
+ return frustrationCount >= 2
219
+ }
220
+
221
+ // ── Internal ──
222
+
223
+ private scorePatterns(
224
+ input: string,
225
+ patterns: Array<{ pattern: RegExp; weight: number; label: string }>,
226
+ recentContext?: string[],
227
+ ): { score: number; triggers: string[] } {
228
+ const triggers: string[] = []
229
+ let totalWeight = 0
230
+ let matchCount = 0
231
+
232
+ for (const p of patterns) {
233
+ if (p.pattern.test(input)) {
234
+ triggers.push(p.label)
235
+ totalWeight += p.weight
236
+ matchCount++
237
+ }
238
+ }
239
+
240
+ // Check recent context for frustration escalation
241
+ if (recentContext && patterns === FRUSTRATION_PATTERNS) {
242
+ const recentFrustration = recentContext
243
+ .slice(-2)
244
+ .filter((ctx) => patterns.some((p) => p.pattern.test(ctx))).length
245
+ if (recentFrustration >= 1) {
246
+ totalWeight += 0.15 // escalation bonus
247
+ }
248
+ }
249
+
250
+ // Normalize: first match passes at weight ≥ 0.5; additional matches increase confidence
251
+ // with diminishing returns (matchCount-1 so single match = no dampening)
252
+ const score =
253
+ matchCount === 0 ? 0 : Math.min(1.0, totalWeight / (1 + Math.max(0, matchCount - 1) * 0.5))
254
+
255
+ return { score, triggers }
256
+ }
257
+ }
@@ -15,6 +15,7 @@ import type { AgentRegistry } from '../agent/agent-registry'
15
15
  import { analyzeForMemory } from './memory/memory-writer'
16
16
  import { getMemoryManager } from './memory/memory-loader'
17
17
  import { AutoMemoryEngine } from './auto-memory.js'
18
+ import type { ToolCallRecord } from './auto-memory.js'
18
19
  import type { AgentViewManager } from '../agent-view/agent-view-manager'
19
20
  import type { SkillsLoader } from '../skills/loader'
20
21
  import { getBackgroundAgentRegistry } from '../agent/background-registry'
@@ -26,6 +27,7 @@ import { ErrorSignatureDB } from './error-signature-db.js'
26
27
  import { PreFlightChecker } from './preflight-checker.js'
27
28
  import { AutoCorrector } from './auto-corrector.js'
28
29
  import { MetaRuleEngine } from './meta-rule-engine.js'
30
+ import { DreamEngine } from './dream-engine.js'
29
31
  import { UsageTracker } from './usage-tracker'
30
32
  import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
31
33
  import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
@@ -55,6 +57,7 @@ export class QueryEngine {
55
57
  private _preflightChecker?: PreFlightChecker
56
58
  private _autoCorrector?: AutoCorrector
57
59
  private _metaRuleEngine?: MetaRuleEngine
60
+ private _dreamEngine?: DreamEngine
58
61
  /** Files read this session — tracks what the Read tool has loaded */
59
62
  private readFiles = new Set<string>()
60
63
  private goal?: string
@@ -160,6 +163,11 @@ export class QueryEngine {
160
163
  this.hookEngine = hooks
161
164
  }
162
165
 
166
+ /** Get the registered hook engine (for health inspection). */
167
+ getHookEngine(): HookEngine | undefined {
168
+ return this.hookEngine
169
+ }
170
+
163
171
  /** Register the artifact server for tool context. */
164
172
  setArtifactServer(server: ArtifactServer): void {
165
173
  this.artifactServer = server
@@ -423,6 +431,7 @@ export class QueryEngine {
423
431
  let thinkingContent = ''
424
432
  let turnApiInputTokens = 0
425
433
  let turnApiOutputTokens = 0
434
+ const turnStart = Date.now()
426
435
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
427
436
 
428
437
  // Stream model response
@@ -529,7 +538,9 @@ export class QueryEngine {
529
538
  }
530
539
 
531
540
  // Execute any tools that were requested
541
+ const toolCallRecords: ToolCallRecord[] = []
532
542
  for (const toolUse of toolUses) {
543
+ const toolStart = Date.now()
533
544
  const result = await this.executeTool(toolUse.name, toolUse.input)
534
545
  yield {
535
546
  type: 'tool_result',
@@ -537,6 +548,15 @@ export class QueryEngine {
537
548
  content: result.success ? result.content : result.error || result.content,
538
549
  }
539
550
 
551
+ // Collect tool call record for CRSI auto-reflection
552
+ toolCallRecords.push({
553
+ name: toolUse.name,
554
+ input: toolUse.input,
555
+ success: result.success,
556
+ error: result.success ? undefined : result.error || 'Unknown error',
557
+ durationMs: Date.now() - toolStart,
558
+ })
559
+
540
560
  // Add tool use + result to context
541
561
  // DeepSeek V4 thinking mode requires reasoning_content on every assistant message
542
562
  this.context.addMessage({
@@ -556,6 +576,28 @@ export class QueryEngine {
556
576
  })
557
577
  }
558
578
 
579
+ // ── CRSI Reflection: Wire AutoMemoryEngine.analyzeTurn() into main loop ──
580
+ // This is the primary CRSI learning mechanism — every turn's tool calls,
581
+ // successes, and failures are analyzed for patterns and fed into the
582
+ // CRSI pipeline (PatternAnalyzer → ExperienceRuleEngine → ErrorSignatureDB).
583
+ if (toolCallRecords.length > 0) {
584
+ try {
585
+ const autoMemory = this.getAutoMemory()
586
+ const reflection = autoMemory.analyzeTurn({
587
+ sessionId: this.sessionId,
588
+ userMessage: userInput,
589
+ assistantContent,
590
+ toolCalls: toolCallRecords,
591
+ modelProvider: this.registry.getActive().config.id,
592
+ modelId: this.registry.getActiveModel(),
593
+ turnDurationMs: turnStart ? Date.now() - turnStart : 0,
594
+ })
595
+ autoMemory.persist(reflection)
596
+ } catch {
597
+ // Reflection is non-critical — failures must not break the main loop
598
+ }
599
+ }
600
+
559
601
  // Record API token usage for this turn, attributed to executed tools
560
602
  if (turnApiInputTokens > 0 || turnApiOutputTokens > 0) {
561
603
  if (toolUses.length > 0) {
@@ -1136,6 +1178,14 @@ export class QueryEngine {
1136
1178
  return this._metaRuleEngine
1137
1179
  }
1138
1180
 
1181
+ /** Auto-Dream: Lazily-initialized background memory consolidation engine. */
1182
+ getDreamEngine(): DreamEngine {
1183
+ if (!this._dreamEngine) {
1184
+ this._dreamEngine = new DreamEngine()
1185
+ }
1186
+ return this._dreamEngine
1187
+ }
1188
+
1139
1189
  /** Register a tool dynamically (used by MCP auto-registration). */
1140
1190
  registerTool(tool: ToolDefinition): void {
1141
1191
  if (this.tools.has(tool.name)) {
package/src/core/hooks.ts CHANGED
@@ -6,6 +6,29 @@ import type {
6
6
  ToolResult,
7
7
  } from '../shared/index.ts'
8
8
 
9
+ // ── Hook resilience types ──
10
+
11
+ interface HookHealth {
12
+ /** Consecutive failure count */
13
+ failures: number
14
+ /** Timestamp of last failure (ms since epoch) */
15
+ lastFailureTime: number
16
+ /** Whether the hook is currently disabled due to repeated failures */
17
+ disabled: boolean
18
+ /** Timestamp when disabled (ms since epoch) */
19
+ disabledAt: number
20
+ /** Total failure count (lifetime) */
21
+ totalFailures: number
22
+ }
23
+
24
+ /** Max consecutive failures before auto-disabling a hook */
25
+ const MAX_CONSECUTIVE_FAILURES = 5
26
+
27
+ /** Cooldown period in ms before a disabled hook is re-tried (5 minutes) */
28
+ const COOLDOWN_MS = 5 * 60 * 1000
29
+
30
+ // ── Helpers ──
31
+
9
32
  /**
10
33
  * Merge two permission decisions, keeping the more restrictive one.
11
34
  * Priority: deny > defer > ask > allow
@@ -22,6 +45,8 @@ export function mergePermissionDecision(
22
45
 
23
46
  export class HookEngine {
24
47
  private hooks: HookDefinition[] = []
48
+ /** Health tracking per hook key (event[:toolName]) */
49
+ private health = new Map<string, HookHealth>()
25
50
 
26
51
  register(hook: HookDefinition): void {
27
52
  this.hooks.push(hook)
@@ -71,31 +96,26 @@ export class HookEngine {
71
96
 
72
97
  // ── NEW event executors ──
73
98
 
74
- /** Stop hook: fires when AI completes a turn. Can block to force continuation. */
75
99
  async executeStop(sessionId: string): Promise<HookResult> {
76
100
  const ctx: HookContext = { event: 'Stop', sessionId }
77
101
  return this.runHooks('Stop', undefined, ctx)
78
102
  }
79
103
 
80
- /** UserPromptSubmit: fires when user submits input. Can block malicious input. */
81
104
  async executeUserPromptSubmit(prompt: string, sessionId: string): Promise<HookResult> {
82
105
  const ctx: HookContext = { event: 'UserPromptSubmit', sessionId, userPrompt: prompt }
83
106
  return this.runHooks('UserPromptSubmit', undefined, ctx)
84
107
  }
85
108
 
86
- /** PreCompact: fires before context compaction. */
87
109
  async executePreCompact(sessionId: string): Promise<HookResult> {
88
110
  const ctx: HookContext = { event: 'PreCompact', sessionId }
89
111
  return this.runHooks('PreCompact', undefined, ctx)
90
112
  }
91
113
 
92
- /** PostCompact: fires after context compaction. Can inject additional context. */
93
114
  async executePostCompact(sessionId: string): Promise<HookResult> {
94
115
  const ctx: HookContext = { event: 'PostCompact', sessionId }
95
116
  return this.runHooks('PostCompact', undefined, ctx)
96
117
  }
97
118
 
98
- /** ConfigChange: fires when configuration changes. */
99
119
  async executeConfigChange(key: string, value: unknown, sessionId: string): Promise<HookResult> {
100
120
  const ctx: HookContext = {
101
121
  event: 'ConfigChange',
@@ -106,7 +126,6 @@ export class HookEngine {
106
126
  return this.runHooks('ConfigChange', undefined, ctx)
107
127
  }
108
128
 
109
- /** SubagentStart: fires when a sub-agent begins execution. */
110
129
  async executeSubagentStart(
111
130
  agentType: string,
112
131
  description: string,
@@ -120,7 +139,6 @@ export class HookEngine {
120
139
  return this.runHooks('SubagentStart', undefined, ctx)
121
140
  }
122
141
 
123
- /** SubagentStop: fires when a sub-agent completes (success or failure). */
124
142
  async executeSubagentStop(
125
143
  agentType: string,
126
144
  description: string,
@@ -137,7 +155,6 @@ export class HookEngine {
137
155
  return this.runHooks('SubagentStop', undefined, ctx)
138
156
  }
139
157
 
140
- /** PostToolUseFailure: fires when a tool call fails. */
141
158
  async executePostToolUseFailure(
142
159
  toolName: string,
143
160
  toolInput: Record<string, unknown>,
@@ -154,7 +171,6 @@ export class HookEngine {
154
171
  return this.runHooks('PostToolUseFailure', toolName, ctx)
155
172
  }
156
173
 
157
- /** PreInference: fires before every model API call for DLP inspection. */
158
174
  async executePreInference(
159
175
  messages: Array<{ role: string; content: string }>,
160
176
  toolCalls: Array<{
@@ -177,6 +193,79 @@ export class HookEngine {
177
193
  return this.runHooks('PreInference', undefined, ctx)
178
194
  }
179
195
 
196
+ // ── Health & Resilience ──
197
+
198
+ /** Get a hook health key for tracking. */
199
+ private healthKey(hook: HookDefinition): string {
200
+ return hook.toolName ? `${hook.event}:${hook.toolName}` : hook.event
201
+ }
202
+
203
+ /** Check if a hook should be skipped due to repeated failures. */
204
+ private shouldSkip(key: string): boolean {
205
+ const h = this.health.get(key)
206
+ if (!h) return false
207
+
208
+ // If disabled, check cooldown
209
+ if (h.disabled) {
210
+ const elapsed = Date.now() - h.disabledAt
211
+ if (elapsed < COOLDOWN_MS) {
212
+ return true // still in cooldown
213
+ }
214
+ // Cooldown expired — re-enable for a trial
215
+ h.disabled = false
216
+ h.failures = 0
217
+ process.stderr.write(`🔄 Hook "${key}" cooldown expired — re-enabled for trial.\n`)
218
+ return false
219
+ }
220
+
221
+ return false
222
+ }
223
+
224
+ /** Record a hook success — resets failure counter. */
225
+ private recordSuccess(key: string): void {
226
+ const h = this.health.get(key)
227
+ if (h) {
228
+ h.failures = 0
229
+ h.disabled = false
230
+ }
231
+ }
232
+
233
+ /** Record a hook failure — may trigger auto-disable. */
234
+ private recordFailure(key: string): void {
235
+ let h = this.health.get(key)
236
+ if (!h) {
237
+ h = { failures: 0, lastFailureTime: 0, disabled: false, disabledAt: 0, totalFailures: 0 }
238
+ this.health.set(key, h)
239
+ }
240
+
241
+ h.failures++
242
+ h.totalFailures++
243
+ h.lastFailureTime = Date.now()
244
+
245
+ if (h.failures >= MAX_CONSECUTIVE_FAILURES && !h.disabled) {
246
+ h.disabled = true
247
+ h.disabledAt = Date.now()
248
+ process.stderr.write(
249
+ `⚠️ Hook "${key}" failed ${MAX_CONSECUTIVE_FAILURES} consecutive times — auto-disabled for ${COOLDOWN_MS / 60_000} minutes.\n` +
250
+ ` Use /hooks enable "${key}" to re-enable manually.\n`,
251
+ )
252
+ }
253
+ }
254
+
255
+ /** Get the health status of all hooks. */
256
+ getHookHealth(): Array<{ key: string; health: HookHealth }> {
257
+ return Array.from(this.health.entries()).map(([key, health]) => ({ key, health }))
258
+ }
259
+
260
+ /** Manually re-enable a disabled hook. */
261
+ reEnableHook(key: string): boolean {
262
+ const h = this.health.get(key)
263
+ if (!h || !h.disabled) return false
264
+ h.disabled = false
265
+ h.failures = 0
266
+ return true
267
+ }
268
+
180
269
  // ── Core execution ──
181
270
 
182
271
  private async runHooks(
@@ -191,9 +280,23 @@ export class HookEngine {
191
280
  const result: HookResult = { allowed: true }
192
281
 
193
282
  for (const hook of matching) {
283
+ const key = this.healthKey(hook)
284
+
285
+ // ── Resilience: skip disabled hooks ──
286
+ if (this.shouldSkip(key)) {
287
+ result.additionalContext = result.additionalContext
288
+ ? result.additionalContext +
289
+ `\n[Hook "${key}" skipped — temporarily disabled after repeated failures]`
290
+ : `[Hook "${key}" skipped — temporarily disabled after repeated failures]`
291
+ continue
292
+ }
293
+
194
294
  try {
195
295
  const hookResult = await hook.handler(ctx)
196
296
 
297
+ // Record success
298
+ this.recordSuccess(key)
299
+
197
300
  // Block on first deny — stops further hook execution
198
301
  if (!hookResult.allowed) {
199
302
  return { ...hookResult }
@@ -233,6 +336,8 @@ export class HookEngine {
233
336
  result.updatedOutput = hookResult.updatedOutput
234
337
  }
235
338
  } catch {
339
+ // Record failure — may trigger auto-disable
340
+ this.recordFailure(key)
236
341
  // Hook failures do not block execution
237
342
  }
238
343
  }