pumuki 6.3.37 → 6.3.39
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/VERSION +1 -1
- package/docs/README.md +1 -1
- package/docs/RELEASE_NOTES.md +53 -0
- package/docs/registro-maestro-de-seguimiento.md +7 -6
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +129 -0
- package/docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md +27 -3
- package/integrations/gate/evaluateAiGate.ts +20 -2
- package/integrations/git/GitService.ts +28 -1
- package/integrations/git/getCommitRangeFacts.ts +35 -5
- package/integrations/git/gitAtomicity.ts +274 -0
- package/integrations/git/runPlatformGate.ts +86 -0
- package/integrations/git/stageRunners.ts +193 -4
- package/integrations/lifecycle/adapter.templates.json +20 -20
- package/integrations/lifecycle/cli.ts +50 -1
- package/integrations/lifecycle/doctor.ts +17 -4
- package/integrations/lifecycle/hookBlock.ts +37 -11
- package/integrations/lifecycle/hookManager.ts +85 -1
- package/integrations/lifecycle/packageInfo.ts +27 -1
- package/integrations/lifecycle/status.ts +7 -2
- package/integrations/mcp/autoExecuteAiStart.ts +116 -0
- package/integrations/mcp/enterpriseServer.ts +56 -0
- package/integrations/mcp/preFlightCheck.ts +108 -0
- package/integrations/notifications/emitAuditSummaryNotification.ts +28 -0
- package/package.json +1 -1
- package/scripts/framework-menu-consumer-preflight-lib.ts +11 -0
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -1
- package/scripts/framework-menu-system-notifications-lib.ts +281 -17
|
@@ -15,6 +15,9 @@ export type PumukiCriticalNotificationEvent =
|
|
|
15
15
|
kind: 'gate.blocked';
|
|
16
16
|
stage: PumukiNotificationStage;
|
|
17
17
|
totalViolations: number;
|
|
18
|
+
causeCode?: string;
|
|
19
|
+
causeMessage?: string;
|
|
20
|
+
remediation?: string;
|
|
18
21
|
}
|
|
19
22
|
| {
|
|
20
23
|
kind: 'evidence.stale';
|
|
@@ -31,23 +34,36 @@ export type SystemNotificationPayload = {
|
|
|
31
34
|
title: string;
|
|
32
35
|
message: string;
|
|
33
36
|
subtitle?: string;
|
|
37
|
+
soundName?: string;
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
export type SystemNotificationsConfig = {
|
|
37
41
|
enabled: boolean;
|
|
38
42
|
channel: 'macos';
|
|
43
|
+
muteUntil?: string;
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
export type SystemNotificationEmitResult =
|
|
42
47
|
| { delivered: true; reason: 'delivered' }
|
|
43
|
-
| { delivered: false; reason: 'disabled' | 'unsupported-platform' | 'command-failed' };
|
|
48
|
+
| { delivered: false; reason: 'disabled' | 'muted' | 'unsupported-platform' | 'command-failed' };
|
|
44
49
|
|
|
45
50
|
type SystemNotificationCommandRunner = (
|
|
46
51
|
command: string,
|
|
47
52
|
args: ReadonlyArray<string>
|
|
48
53
|
) => number;
|
|
49
54
|
|
|
55
|
+
type SystemNotificationCommandRunnerWithOutput = (
|
|
56
|
+
command: string,
|
|
57
|
+
args: ReadonlyArray<string>
|
|
58
|
+
) => {
|
|
59
|
+
exitCode: number;
|
|
60
|
+
stdout: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
50
63
|
const SYSTEM_NOTIFICATIONS_CONFIG_PATH = '.pumuki/system-notifications.json';
|
|
64
|
+
const BLOCKED_DIALOG_KEEP = 'Mantener activas';
|
|
65
|
+
const BLOCKED_DIALOG_MUTE_30 = 'Silenciar 30 min';
|
|
66
|
+
const BLOCKED_DIALOG_DISABLE = 'Desactivar';
|
|
51
67
|
|
|
52
68
|
const escapeAppleScriptString = (value: string): string =>
|
|
53
69
|
value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ').trim();
|
|
@@ -58,7 +74,132 @@ const buildDisplayNotificationScript = (payload: SystemNotificationPayload): str
|
|
|
58
74
|
const subtitleFragment = payload.subtitle
|
|
59
75
|
? ` subtitle "${escapeAppleScriptString(payload.subtitle)}"`
|
|
60
76
|
: '';
|
|
61
|
-
|
|
77
|
+
const soundFragment = payload.soundName
|
|
78
|
+
? ` sound name "${escapeAppleScriptString(payload.soundName)}"`
|
|
79
|
+
: '';
|
|
80
|
+
return `display notification "${message}" with title "${title}"${subtitleFragment}${soundFragment}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const buildDisplayDialogScript = (params: {
|
|
84
|
+
title: string;
|
|
85
|
+
cause: string;
|
|
86
|
+
remediation: string;
|
|
87
|
+
}): string => {
|
|
88
|
+
const title = escapeAppleScriptString(params.title);
|
|
89
|
+
const cause = escapeAppleScriptString(params.cause);
|
|
90
|
+
const remediation = escapeAppleScriptString(params.remediation);
|
|
91
|
+
const message = escapeAppleScriptString(`Causa: ${cause}\n\nSolución: ${remediation}`);
|
|
92
|
+
return `display dialog "${message}" with title "${title}" buttons {"${BLOCKED_DIALOG_DISABLE}", "${BLOCKED_DIALOG_MUTE_30}", "${BLOCKED_DIALOG_KEEP}"} default button "${BLOCKED_DIALOG_KEEP}" with icon stop giving up after 15`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizeNotificationText = (value: string): string =>
|
|
96
|
+
value.replace(/\s+/g, ' ').trim();
|
|
97
|
+
|
|
98
|
+
const truncateNotificationText = (value: string, maxLength: number): string => {
|
|
99
|
+
if (value.length <= maxLength) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
return `${value.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const isTruthyFlag = (value?: string): boolean => {
|
|
106
|
+
if (!value) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const normalized = value.trim().toLowerCase();
|
|
110
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
114
|
+
EVIDENCE_MISSING: 'Falta evidencia para validar este paso.',
|
|
115
|
+
EVIDENCE_INVALID: 'La evidencia actual es inválida.',
|
|
116
|
+
EVIDENCE_CHAIN_INVALID: 'La cadena de evidencia no es válida.',
|
|
117
|
+
EVIDENCE_STALE: 'La evidencia está desactualizada.',
|
|
118
|
+
EVIDENCE_BRANCH_MISMATCH: 'La evidencia no corresponde con la rama actual.',
|
|
119
|
+
EVIDENCE_REPO_ROOT_MISMATCH: 'La evidencia no corresponde con este repositorio.',
|
|
120
|
+
PRE_PUSH_UPSTREAM_MISSING: 'La rama no tiene upstream configurado.',
|
|
121
|
+
GITFLOW_PROTECTED_BRANCH: 'No se permiten cambios directos en esta rama protegida.',
|
|
122
|
+
SDD_SESSION_MISSING: 'No hay sesión SDD activa.',
|
|
123
|
+
SDD_SESSION_INVALID: 'La sesión SDD actual no es válida.',
|
|
124
|
+
OPENSPEC_MISSING: 'OpenSpec no está instalado en este repositorio.',
|
|
125
|
+
MCP_ENTERPRISE_RECEIPT_MISSING: 'Falta el recibo enterprise de MCP.',
|
|
126
|
+
BACKEND_AVOID_EXPLICIT_ANY: 'Se detectó uso de "any" explícito en backend.',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
130
|
+
EVIDENCE_MISSING: 'Genera evidencia y vuelve a ejecutar el gate.',
|
|
131
|
+
EVIDENCE_INVALID: 'Regenera la evidencia antes de reintentar.',
|
|
132
|
+
EVIDENCE_CHAIN_INVALID: 'Regenera evidencia para reparar la cadena criptográfica.',
|
|
133
|
+
EVIDENCE_STALE: 'Refresca la evidencia y vuelve a intentarlo.',
|
|
134
|
+
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en esta rama y reintenta.',
|
|
135
|
+
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este repositorio.',
|
|
136
|
+
PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>.',
|
|
137
|
+
SDD_SESSION_MISSING: 'Abre sesión SDD y vuelve a intentar.',
|
|
138
|
+
SDD_SESSION_INVALID: 'Refresca la sesión SDD y vuelve a intentar.',
|
|
139
|
+
OPENSPEC_MISSING: 'Instala OpenSpec y reintenta la validación.',
|
|
140
|
+
MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP antes de continuar.',
|
|
141
|
+
BACKEND_AVOID_EXPLICIT_ANY: 'Tipa el valor y elimina "any" explícito en backend.',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const toKnownSpanishCauseFromMessage = (message: string): string | null => {
|
|
145
|
+
const normalized = message.toLowerCase();
|
|
146
|
+
if (normalized.includes('avoid explicit any')) {
|
|
147
|
+
return BLOCKED_CAUSE_SUMMARY_BY_CODE.BACKEND_AVOID_EXPLICIT_ANY;
|
|
148
|
+
}
|
|
149
|
+
if (normalized.includes('evidence is stale')) {
|
|
150
|
+
return BLOCKED_CAUSE_SUMMARY_BY_CODE.EVIDENCE_STALE;
|
|
151
|
+
}
|
|
152
|
+
if (normalized.includes('no upstream tracking reference')) {
|
|
153
|
+
return BLOCKED_CAUSE_SUMMARY_BY_CODE.PRE_PUSH_UPSTREAM_MISSING;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const toKnownSpanishRemediationFromMessage = (message: string): string | null => {
|
|
159
|
+
const normalized = message.toLowerCase();
|
|
160
|
+
if (normalized.includes('avoid explicit any')) {
|
|
161
|
+
return BLOCKED_REMEDIATION_BY_CODE.BACKEND_AVOID_EXPLICIT_ANY;
|
|
162
|
+
}
|
|
163
|
+
if (normalized.includes('set-upstream')) {
|
|
164
|
+
return BLOCKED_REMEDIATION_BY_CODE.PRE_PUSH_UPSTREAM_MISSING;
|
|
165
|
+
}
|
|
166
|
+
if (normalized.includes('refresh evidence')) {
|
|
167
|
+
return BLOCKED_REMEDIATION_BY_CODE.EVIDENCE_STALE;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const resolveBlockedCauseSummary = (event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>, causeCode: string): string => {
|
|
173
|
+
const mapped = BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode];
|
|
174
|
+
if (mapped) {
|
|
175
|
+
return mapped;
|
|
176
|
+
}
|
|
177
|
+
if (event.causeMessage && event.causeMessage.trim().length > 0) {
|
|
178
|
+
const rawMessage = normalizeNotificationText(event.causeMessage).replace(/^[A-Z0-9_]+:\s*/, '');
|
|
179
|
+
const translated = toKnownSpanishCauseFromMessage(rawMessage);
|
|
180
|
+
if (translated) {
|
|
181
|
+
return translated;
|
|
182
|
+
}
|
|
183
|
+
return truncateNotificationText(rawMessage, 72);
|
|
184
|
+
}
|
|
185
|
+
return `Se detectaron ${event.totalViolations} bloqueos en ${event.stage}.`;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const resolveBlockedRemediation = (event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>, causeCode: string): string => {
|
|
189
|
+
const fromEvent = event.remediation
|
|
190
|
+
? normalizeNotificationText(event.remediation).replace(/^cómo solucionarlo:\s*/i, '').replace(/^remediation:\s*/i, '')
|
|
191
|
+
: '';
|
|
192
|
+
if (fromEvent.length > 0) {
|
|
193
|
+
const translated = toKnownSpanishRemediationFromMessage(fromEvent);
|
|
194
|
+
if (translated) {
|
|
195
|
+
return truncateNotificationText(translated, 88);
|
|
196
|
+
}
|
|
197
|
+
return truncateNotificationText(fromEvent, 88);
|
|
198
|
+
}
|
|
199
|
+
const fallback =
|
|
200
|
+
BLOCKED_REMEDIATION_BY_CODE[causeCode]
|
|
201
|
+
?? 'Corrige el bloqueo indicado y vuelve a ejecutar el comando.';
|
|
202
|
+
return truncateNotificationText(fallback, 88);
|
|
62
203
|
};
|
|
63
204
|
|
|
64
205
|
const runSystemCommand: SystemNotificationCommandRunner = (command, args) => {
|
|
@@ -70,6 +211,43 @@ const runSystemCommand: SystemNotificationCommandRunner = (command, args) => {
|
|
|
70
211
|
}
|
|
71
212
|
};
|
|
72
213
|
|
|
214
|
+
const runSystemCommandWithOutput: SystemNotificationCommandRunnerWithOutput = (
|
|
215
|
+
command,
|
|
216
|
+
args
|
|
217
|
+
) => {
|
|
218
|
+
try {
|
|
219
|
+
const stdout = runBinarySync(command, [...args], {
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
exitCode: 0,
|
|
224
|
+
stdout: typeof stdout === 'string' ? stdout : '',
|
|
225
|
+
};
|
|
226
|
+
} catch (error: unknown) {
|
|
227
|
+
const fallbackStdout =
|
|
228
|
+
typeof error === 'object' &&
|
|
229
|
+
error !== null &&
|
|
230
|
+
'stdout' in error &&
|
|
231
|
+
typeof (error as { stdout?: unknown }).stdout === 'string'
|
|
232
|
+
? (error as { stdout: string }).stdout
|
|
233
|
+
: '';
|
|
234
|
+
return {
|
|
235
|
+
exitCode: 1,
|
|
236
|
+
stdout: fallbackStdout,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const persistSystemNotificationsConfigFile = (
|
|
242
|
+
repoRoot: string,
|
|
243
|
+
config: SystemNotificationsConfig
|
|
244
|
+
): string => {
|
|
245
|
+
const configPath = join(repoRoot, SYSTEM_NOTIFICATIONS_CONFIG_PATH);
|
|
246
|
+
mkdirSync(join(repoRoot, '.pumuki'), { recursive: true });
|
|
247
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
248
|
+
return configPath;
|
|
249
|
+
};
|
|
250
|
+
|
|
73
251
|
export const buildSystemNotificationsConfigFromSelection = (
|
|
74
252
|
enabled: boolean
|
|
75
253
|
): SystemNotificationsConfig => ({
|
|
@@ -78,14 +256,10 @@ export const buildSystemNotificationsConfigFromSelection = (
|
|
|
78
256
|
});
|
|
79
257
|
|
|
80
258
|
export const persistSystemNotificationsConfig = (repoRoot: string, enabled: boolean): string => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
configPath,
|
|
85
|
-
`${JSON.stringify(buildSystemNotificationsConfigFromSelection(enabled), null, 2)}\n`,
|
|
86
|
-
'utf8'
|
|
259
|
+
return persistSystemNotificationsConfigFile(
|
|
260
|
+
repoRoot,
|
|
261
|
+
buildSystemNotificationsConfigFromSelection(enabled)
|
|
87
262
|
);
|
|
88
|
-
return configPath;
|
|
89
263
|
};
|
|
90
264
|
|
|
91
265
|
export const readSystemNotificationsConfig = (repoRoot: string): SystemNotificationsConfig => {
|
|
@@ -98,16 +272,67 @@ export const readSystemNotificationsConfig = (repoRoot: string): SystemNotificat
|
|
|
98
272
|
const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
|
99
273
|
enabled?: unknown;
|
|
100
274
|
channel?: unknown;
|
|
275
|
+
muteUntil?: unknown;
|
|
101
276
|
};
|
|
102
|
-
|
|
277
|
+
const config: SystemNotificationsConfig = {
|
|
103
278
|
enabled: parsed.enabled === true,
|
|
104
279
|
channel: 'macos',
|
|
105
280
|
};
|
|
281
|
+
if (typeof parsed.muteUntil === 'string' && parsed.muteUntil.trim().length > 0) {
|
|
282
|
+
config.muteUntil = parsed.muteUntil;
|
|
283
|
+
}
|
|
284
|
+
return config;
|
|
106
285
|
} catch {
|
|
107
286
|
return buildSystemNotificationsConfigFromSelection(true);
|
|
108
287
|
}
|
|
109
288
|
};
|
|
110
289
|
|
|
290
|
+
const isMutedAt = (config: SystemNotificationsConfig, nowMs: number): boolean => {
|
|
291
|
+
if (!config.muteUntil) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const parsed = Date.parse(config.muteUntil);
|
|
295
|
+
if (!Number.isFinite(parsed)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return parsed > nowMs;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const extractDialogButton = (stdout: string): string | null => {
|
|
302
|
+
const match = stdout.match(/button returned:(.+)/i);
|
|
303
|
+
if (!match || !match[1]) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return match[1].trim();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const applyDialogChoice = (params: {
|
|
310
|
+
repoRoot: string;
|
|
311
|
+
config: SystemNotificationsConfig;
|
|
312
|
+
button: string;
|
|
313
|
+
nowMs: number;
|
|
314
|
+
}): void => {
|
|
315
|
+
if (params.button === BLOCKED_DIALOG_KEEP) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (params.button === BLOCKED_DIALOG_DISABLE) {
|
|
319
|
+
persistSystemNotificationsConfigFile(params.repoRoot, {
|
|
320
|
+
enabled: false,
|
|
321
|
+
channel: params.config.channel,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (params.button === BLOCKED_DIALOG_MUTE_30) {
|
|
326
|
+
const minutes = 30;
|
|
327
|
+
const muteUntil = new Date(params.nowMs + minutes * 60_000).toISOString();
|
|
328
|
+
persistSystemNotificationsConfigFile(params.repoRoot, {
|
|
329
|
+
enabled: true,
|
|
330
|
+
channel: params.config.channel,
|
|
331
|
+
muteUntil,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
111
336
|
export const buildSystemNotificationPayload = (
|
|
112
337
|
event: PumukiCriticalNotificationEvent
|
|
113
338
|
): SystemNotificationPayload => {
|
|
@@ -137,23 +362,30 @@ export const buildSystemNotificationPayload = (
|
|
|
137
362
|
}
|
|
138
363
|
|
|
139
364
|
if (event.kind === 'gate.blocked') {
|
|
365
|
+
const causeCode = event.causeCode ?? 'GATE_BLOCKED';
|
|
366
|
+
const causeSummary = truncateNotificationText(
|
|
367
|
+
resolveBlockedCauseSummary(event, causeCode),
|
|
368
|
+
72
|
|
369
|
+
);
|
|
370
|
+
const remediation = resolveBlockedRemediation(event, causeCode);
|
|
140
371
|
return {
|
|
141
|
-
title: 'Pumuki
|
|
142
|
-
subtitle: event.stage
|
|
143
|
-
message: `
|
|
372
|
+
title: '🔴 Pumuki bloqueado',
|
|
373
|
+
subtitle: `${event.stage} · ${causeSummary}`,
|
|
374
|
+
message: `Solución: ${remediation}`,
|
|
375
|
+
soundName: 'Basso',
|
|
144
376
|
};
|
|
145
377
|
}
|
|
146
378
|
|
|
147
379
|
if (event.kind === 'evidence.stale') {
|
|
148
380
|
return {
|
|
149
|
-
title: 'Pumuki ·
|
|
150
|
-
message:
|
|
381
|
+
title: '🟡 Pumuki · evidencia desactualizada',
|
|
382
|
+
message: `Actualiza evidencia (${event.ageMinutes} min): ${event.evidencePath}.`,
|
|
151
383
|
};
|
|
152
384
|
}
|
|
153
385
|
|
|
154
386
|
return {
|
|
155
|
-
title: 'Pumuki ·
|
|
156
|
-
message: `
|
|
387
|
+
title: '🔴 Pumuki · bloqueo GitFlow',
|
|
388
|
+
message: `La rama ${event.currentBranch} no cumple GitFlow (${event.reason}).`,
|
|
157
389
|
};
|
|
158
390
|
};
|
|
159
391
|
|
|
@@ -161,8 +393,11 @@ export const emitSystemNotification = (params: {
|
|
|
161
393
|
event: PumukiCriticalNotificationEvent;
|
|
162
394
|
platform?: NodeJS.Platform;
|
|
163
395
|
runCommand?: SystemNotificationCommandRunner;
|
|
396
|
+
runCommandWithOutput?: SystemNotificationCommandRunnerWithOutput;
|
|
164
397
|
repoRoot?: string;
|
|
165
398
|
config?: SystemNotificationsConfig;
|
|
399
|
+
env?: NodeJS.ProcessEnv;
|
|
400
|
+
now?: () => number;
|
|
166
401
|
}): SystemNotificationEmitResult => {
|
|
167
402
|
const config =
|
|
168
403
|
params.config ??
|
|
@@ -172,6 +407,10 @@ export const emitSystemNotification = (params: {
|
|
|
172
407
|
if (!config.enabled) {
|
|
173
408
|
return { delivered: false, reason: 'disabled' };
|
|
174
409
|
}
|
|
410
|
+
const nowMs = (params.now ?? Date.now)();
|
|
411
|
+
if (isMutedAt(config, nowMs)) {
|
|
412
|
+
return { delivered: false, reason: 'muted' };
|
|
413
|
+
}
|
|
175
414
|
|
|
176
415
|
const platform = params.platform ?? process.platform;
|
|
177
416
|
if (platform !== 'darwin') {
|
|
@@ -187,5 +426,30 @@ export const emitSystemNotification = (params: {
|
|
|
187
426
|
return { delivered: false, reason: 'command-failed' };
|
|
188
427
|
}
|
|
189
428
|
|
|
429
|
+
const env = params.env ?? process.env;
|
|
430
|
+
if (params.event.kind === 'gate.blocked' && isTruthyFlag(env.PUMUKI_MACOS_BLOCKED_DIALOG)) {
|
|
431
|
+
const causeCode = params.event.causeCode ?? 'GATE_BLOCKED';
|
|
432
|
+
const cause = resolveBlockedCauseSummary(params.event, causeCode);
|
|
433
|
+
const remediation = resolveBlockedRemediation(params.event, causeCode);
|
|
434
|
+
const dialogScript = buildDisplayDialogScript({
|
|
435
|
+
title: '🔴 Pumuki bloqueado',
|
|
436
|
+
cause,
|
|
437
|
+
remediation,
|
|
438
|
+
});
|
|
439
|
+
const dialogRunner = params.runCommandWithOutput ?? runSystemCommandWithOutput;
|
|
440
|
+
const dialogResult = dialogRunner('osascript', ['-e', dialogScript]);
|
|
441
|
+
if (dialogResult.exitCode === 0 && params.repoRoot) {
|
|
442
|
+
const selectedButton = extractDialogButton(dialogResult.stdout);
|
|
443
|
+
if (selectedButton) {
|
|
444
|
+
applyDialogChoice({
|
|
445
|
+
repoRoot: params.repoRoot,
|
|
446
|
+
config,
|
|
447
|
+
button: selectedButton,
|
|
448
|
+
nowMs,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
190
454
|
return { delivered: true, reason: 'delivered' };
|
|
191
455
|
};
|