(entry, now = new Date(), context = {})
| 735 | const detail = entry.detail || entry.reason || entry.message || entry.error || ''; |
| 736 | const command = normalizeCommandForMatch(extractBlockedCommand(detail)); |
| 737 | if (!command) return false; |
| 738 | |
| 739 | const blockedAt = new Date(eventTimestamp(entry) || 0).getTime(); |
| 740 | return auditEntries.some((auditEntry) => { |
| 741 | if ((auditEntry.event || '') !== 'tool-call') return false; |
| 742 | const target = normalizeCommandForMatch(auditEntry.target || auditEntry.command || ''); |
| 743 | if (!target || target !== command) return false; |
| 744 | const calledAt = new Date(eventTimestamp(auditEntry) || 0).getTime(); |
| 745 | return calledAt + 5000 >= blockedAt; |
| 746 | }); |
| 747 | } |
| 748 | |
| 749 | function classifyHookProblem(entry, now = new Date(), context = {}) { |
| 750 | const hook = entry.hook || 'hook'; |
| 751 | const actionName = entry.action || entry.outcome || entry.status || 'recorded'; |
| 752 | const detail = entry.detail || entry.reason || entry.message || entry.error || entry.action || 'hook issue recorded'; |
| 753 | const timestamp = eventTimestamp(entry); |
| 754 | const ageMs = timestamp ? now.getTime() - new Date(timestamp).getTime() : 0; |
| 755 | const stale = timestamp ? ageMs > 24 * 60 * 60 * 1000 : false; |
| 756 | const description = truncate(detail, 90); |
| 757 | const text = `${hook} ${actionName} ${detail}`.toLowerCase(); |
| 758 | |
| 759 | let category = 'attention'; |
| 760 | let severity = 'medium'; |
| 761 | let actionable = true; |
| 762 | |
| 763 | if (stale) { |
| 764 | category = 'stale'; |
| 765 | severity = 'low'; |
| 766 | actionable = false; |
| 767 | } else if (actionName === 'error' || actionName === 'parse-fail' || text.includes('unknown error')) { |
| 768 | category = 'hook-failure'; |
| 769 | severity = 'high'; |
| 770 | actionable = true; |
| 771 | } else if (actionName === 'blocked-restricted') { |
| 772 | category = 'restricted-scope-block'; |
| 773 | severity = 'high'; |
| 774 | actionable = true; |
| 775 | } else if ( |
| 776 | hook === 'external-action-gate' && |
| 777 | (actionName === 'first-encounter' || actionName === 'consent-block') && |
| 778 | hasLaterMatchingToolCall(entry, context.auditEntries || []) |
| 779 | ) { |
| 780 | category = 'resolved-approval'; |
| 781 | severity = 'info'; |
| 782 | actionable = false; |
| 783 | } else if ( |
| 784 | hook === 'external-action-gate' && |
| 785 | (actionName === 'first-encounter' || actionName === 'consent-block') && |
| 786 | timestamp && |
| 787 | ageMs > 15 * 60 * 1000 |
| 788 | ) { |
| 789 | category = 'stale-approval'; |
| 790 | severity = 'low'; |
| 791 | actionable = false; |
| 792 | } else if (hook === 'external-action-gate' && (actionName === 'first-encounter' || actionName === 'consent-block')) { |
| 793 | category = 'approval-needed'; |
| 794 | severity = 'medium'; |
no test coverage detected