import type { PumukiCriticalNotificationEvent } from './framework-menu-system-notifications-types'; import { normalizeNotificationText, truncateNotificationText, } from './framework-menu-system-notifications-text'; import { buildNotificationTrackingRemediation, extractNotificationTrackingContext, TRACKING_BLOCKED_REMEDIATION, } from './framework-menu-system-notifications-tracking'; import { buildLegacySkillCauseRemediation } from './framework-menu-system-notifications-legacy-skill-cause'; type BlockedRemediationVariant = 'banner' | 'dialog'; const BLOCKED_REMEDIATION_BY_CODE: Readonly> = { EVIDENCE_GATE_BLOCKED: 'Revisa status/doctor para ver la causa exacta del gate, corrígela y revalida.', EVIDENCE_MISSING: 'Genera la evidencia del slice actual y vuelve a validar esta fase.', EVIDENCE_INVALID: 'Regenera la evidencia de esta iteración y repite la validación.', EVIDENCE_CHAIN_INVALID: 'Regenera la evidencia para restaurar la cadena de integridad y vuelve a validar.', EVIDENCE_STALE: 'Refresca la evidencia y vuelve a validar PRE_WRITE/PRE_PUSH.', EVIDENCE_BRANCH_MISMATCH: 'La evidencia no corresponde a esta rama. Regenera el receipt/evidencia y vuelve a validar.', EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera la evidencia desde este repositorio y vuelve a validar.', PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin ` y repite PRE_PUSH.', SDD_SESSION_MISSING: 'Abre `openspec/changes/` y copia la carpeta de tu tarea, por ejemplo `rgo-1900-35`. Ejecuta `pumuki sdd session --open --change=` usando ese nombre. Después vuelve a intentar el commit; PRE_WRITE automáticamente.', SDD_SESSION_INVALID: 'Refresca la sesión SDD activa: `pumuki sdd session --refresh --ttl-minutes=90`. Después vuelve a intentar el commit; Pumuki ejecutará PRE_WRITE automáticamente.', OPENSPEC_MISSING: 'Instala OpenSpec en este repositorio y vuelve a validar el gate.', MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a validar.', BACKEND_AVOID_EXPLICIT_ANY: 'Sustituye `any` por tipos concretos en backend y relanza el gate.', GIT_ATOMICITY_TOO_MANY_SCOPES: 'Divide el cambio por scope o en commits más pequeños y vuelve a ejecutar el gate.', SOLID_HEURISTIC: 'Corrige la violación detectada y vuelve a ejecutar el gate.', TRACKING_CANONICAL_IN_PROGRESS_INVALID: TRACKING_BLOCKED_REMEDIATION, TRACKING_CANONICAL_SOURCE_CONFLICT: TRACKING_BLOCKED_REMEDIATION, ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH: 'Ejecuta `pumuki policy reconcile --strict --json` y revalida antes de continuar.', EVIDENCE_PREWRITE_WORKTREE_WARN: 'Reduce el worktree pendiente a un slice atómico antes del commit: stagea solo la tarea activa o guarda el resto en stash nombrado, y vuelve a intentar el commit.', EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT: 'El worktree supera el límite permitido: divide cambios por scope en commits atómicos o stashes nombrados antes de continuar.', }; const BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly> = { banner: 120, dialog: 220, }; const GENERIC_BLOCKED_REMEDIATION = 'Corrige el bloqueo indicado y vuelve a ejecutar el comando.'; const buildMultipleCausesRemediation = (count: number): string => `Corrige las ${count} causas bloqueantes listadas arriba, empezando por violaciones AST/skills de codigo cuando existan, y vuelve a ejecutar el gate.`; const normalizeBlockedRemediation = (value: string): string => normalizeNotificationText(value) .replace(/^cómo solucionarlo:\s*/i, '') .replace(/^remediation:\s*/i, '') .replace(/^solution:\s*/i, '') .replace(/^fix:\s*/i, ''); const resolveFallbackRemediation = (causeCode: string): string => BLOCKED_REMEDIATION_BY_CODE[causeCode] ?? GENERIC_BLOCKED_REMEDIATION; const resolveSddSessionCauseCode = ( event: Extract, causeCode: string ): 'SDD_SESSION_MISSING' | 'SDD_SESSION_INVALID' | null => { if (causeCode === 'SDD_SESSION_MISSING' || causeCode === 'SDD_SESSION_INVALID') { return causeCode; } const searchable = [ event.causeMessage, event.remediation, ...(event.blockingCauses ?? []).flatMap((cause) => [ cause.code, cause.ruleId, cause.message, cause.remediation, ]), ] .filter((value): value is string => typeof value === 'string') .join(' ') .toLowerCase(); if ( searchable.includes('sdd_session_missing') || searchable.includes('no hay sesión sdd activa') || searchable.includes('no hay sesion sdd activa') || searchable.includes('no active sdd session') ) { return 'SDD_SESSION_MISSING'; } if ( searchable.includes('sdd_session_invalid') || searchable.includes('sesión sdd actual no es válida') || searchable.includes('sesion sdd actual no es valida') || searchable.includes('invalid sdd session') ) { return 'SDD_SESSION_INVALID'; } return null; }; const resolveWorktreeHygieneCauseCode = ( event: Extract, causeCode: string ): 'EVIDENCE_PREWRITE_WORKTREE_WARN' | 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT' | null => { if ( causeCode === 'EVIDENCE_PREWRITE_WORKTREE_WARN' || causeCode === 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT' ) { return causeCode; } const searchable = [ event.causeMessage, event.remediation, ...(event.blockingCauses ?? []).flatMap((cause) => [ cause.code, cause.ruleId, cause.message, cause.remediation, ]), ] .filter((value): value is string => typeof value === 'string') .join(' ') .toLowerCase(); if (searchable.includes('evidence_prewrite_worktree_over_limit')) { return 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT'; } if ( searchable.includes('evidence_prewrite_worktree_warn') || searchable.includes('worktree hygiene warning') || searchable.includes('warn_threshold=') ) { return 'EVIDENCE_PREWRITE_WORKTREE_WARN'; } return null; }; const isGenericPolicyReconcileRemediation = (message: string): boolean => { const normalized = message.toLowerCase(); return normalized.includes('policy reconcile') && normalized.includes('sdd validate'); }; const hasEnglishHints = (message: string): boolean => { const normalized = message.toLowerCase(); return [ 'avoid explicit any', 'set-upstream', 'refresh evidence', 'fix and retry', 'split the change', 'smaller commits', 're-run', 'rerun', 'retry', 'to continue', 'run ', ].some((hint) => normalized.includes(hint)); }; const toKnownSpanishRemediationFromMessage = (message: string, causeCode: string): string | null => { const normalized = message.toLowerCase(); if (normalized.includes('avoid explicit any')) { return BLOCKED_REMEDIATION_BY_CODE.BACKEND_AVOID_EXPLICIT_ANY; } if (normalized.includes('set-upstream') || normalized.includes('no upstream tracking reference')) { return BLOCKED_REMEDIATION_BY_CODE.PRE_PUSH_UPSTREAM_MISSING; } if (normalized.includes('refresh evidence') || normalized.includes('evidence is stale')) { return BLOCKED_REMEDIATION_BY_CODE.EVIDENCE_STALE; } if (normalized.includes('split the change')) { return BLOCKED_REMEDIATION_BY_CODE.GIT_ATOMICITY_TOO_MANY_SCOPES; } if (normalized.includes('fix and retry') || normalized.includes('retry')) { return resolveFallbackRemediation(causeCode); } return null; }; export const resolveBlockedRemediation = ( event: Extract, causeCode: string, options?: { variant?: BlockedRemediationVariant; } ): string => { const variant = options?.variant ?? 'dialog'; const maxLength = BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT[variant]; const sddSessionCauseCode = resolveSddSessionCauseCode(event, causeCode); if (sddSessionCauseCode) { return truncateNotificationText( resolveFallbackRemediation(sddSessionCauseCode), maxLength ); } const worktreeHygieneCauseCode = resolveWorktreeHygieneCauseCode(event, causeCode); if (worktreeHygieneCauseCode) { return truncateNotificationText( resolveFallbackRemediation(worktreeHygieneCauseCode), maxLength ); } const blockingCausesCount = event.blockingCauses?.length ?? 0; if (blockingCausesCount > 1) { return truncateNotificationText( buildMultipleCausesRemediation(blockingCausesCount), maxLength ); } const firstCauseWithRemediation = event.blockingCauses?.find( (cause) => cause.remediation && cause.remediation.trim().length > 0 ); if (firstCauseWithRemediation?.remediation) { return truncateNotificationText( normalizeBlockedRemediation(firstCauseWithRemediation.remediation), maxLength ); } const fromEvent = event.remediation ? normalizeBlockedRemediation(event.remediation) : ''; const trackingContext = extractNotificationTrackingContext(event.causeMessage); if (trackingContext) { return truncateNotificationText(buildNotificationTrackingRemediation(trackingContext), maxLength); } const legacySkillCauseRemediation = buildLegacySkillCauseRemediation(event.causeMessage); if (legacySkillCauseRemediation) { return truncateNotificationText(legacySkillCauseRemediation, maxLength); } if (fromEvent.length > 0) { if ( causeCode === 'EVIDENCE_GATE_BLOCKED' && isGenericPolicyReconcileRemediation(fromEvent) ) { return truncateNotificationText(resolveFallbackRemediation(causeCode), maxLength); } const translated = toKnownSpanishRemediationFromMessage(fromEvent, causeCode); if (translated) { return truncateNotificationText(translated, maxLength); } if (hasEnglishHints(fromEvent)) { return truncateNotificationText(resolveFallbackRemediation(causeCode), maxLength); } return truncateNotificationText(fromEvent, maxLength); } return truncateNotificationText(resolveFallbackRemediation(causeCode), maxLength); };