pumuki 6.3.97 → 6.3.99
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/AGENTS.md +269 -0
- package/CHANGELOG.md +697 -0
- package/README.md +4 -2
- package/VERSION +1 -1
- package/docs/README.md +13 -9
- package/docs/operations/RELEASE_NOTES.md +12 -76
- package/docs/product/HOW_IT_WORKS.md +6 -0
- package/docs/product/INSTALLATION.md +1 -1
- package/docs/product/USAGE.md +41 -4
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +118 -0
- package/docs/validation/README.md +6 -3
- package/integrations/config/skillsCustomRules.ts +18 -99
- package/integrations/evidence/buildEvidence.ts +0 -24
- package/integrations/evidence/repoState.ts +0 -3
- package/integrations/evidence/schema.ts +0 -18
- package/integrations/evidence/writeEvidence.ts +0 -24
- package/integrations/gate/evaluateAiGate.ts +15 -232
- package/integrations/gate/remediationCatalog.ts +0 -8
- package/integrations/git/GitService.ts +44 -5
- package/integrations/git/aiGateRepoPolicyFindings.ts +0 -4
- package/integrations/git/runPlatformGate.ts +1 -9
- package/integrations/git/runPlatformGateFacts.ts +19 -1
- package/integrations/git/runPlatformGateOutput.ts +27 -36
- package/integrations/lifecycle/adapter.templates.json +7 -13
- package/integrations/lifecycle/adapter.ts +0 -24
- package/integrations/lifecycle/artifacts.ts +1 -6
- package/integrations/lifecycle/audit.ts +101 -0
- package/integrations/lifecycle/cli.ts +110 -70
- package/integrations/lifecycle/cliSdd.ts +13 -8
- package/integrations/lifecycle/doctor.ts +16 -48
- package/integrations/lifecycle/hookManager.ts +0 -77
- package/integrations/lifecycle/index.ts +2 -0
- package/integrations/lifecycle/install.ts +0 -21
- package/integrations/lifecycle/npmService.ts +3 -155
- package/integrations/lifecycle/policyValidationSnapshot.ts +8 -2
- package/integrations/lifecycle/preWriteAutomation.ts +7 -77
- package/integrations/lifecycle/state.ts +1 -8
- package/integrations/lifecycle/status.ts +2 -29
- package/integrations/mcp/aiGateCheck.ts +26 -206
- package/integrations/mcp/autoExecuteAiStart.ts +87 -94
- package/integrations/mcp/enterpriseServer.ts +7 -23
- package/integrations/mcp/enterpriseStdioServer.cli.ts +4 -31
- package/integrations/mcp/preFlightCheck.ts +5 -51
- package/integrations/platform/detectPlatforms.ts +37 -0
- package/integrations/policy/experimentalFeatures.ts +1 -1
- package/integrations/sdd/evidenceScaffold.ts +2 -109
- package/package.json +10 -2
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
- package/scripts/consumer-postinstall-resolve-args.cjs +44 -0
- package/scripts/consumer-postinstall.cjs +76 -21
- package/scripts/framework-menu-advanced-view-lib.ts +0 -15
- package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
- package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
- package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
- package/scripts/framework-menu-consumer-preflight-run.ts +0 -23
- package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
- package/scripts/framework-menu-consumer-runtime-actions.ts +87 -17
- package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +2 -10
- package/scripts/framework-menu-consumer-runtime-menu.ts +4 -18
- package/scripts/framework-menu-consumer-runtime-types.ts +3 -3
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
- package/scripts/framework-menu-evidence-summary-read.ts +57 -5
- package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
- package/scripts/framework-menu-evidence-summary-types.ts +7 -0
- package/scripts/framework-menu-gate-lib.ts +9 -0
- package/scripts/framework-menu-layout-data.ts +5 -0
- package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
- package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
- package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
- package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
- package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
- package/scripts/framework-menu-system-notifications-cause.ts +0 -24
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +24 -204
- package/scripts/framework-menu-system-notifications-macos.ts +4 -0
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
- package/scripts/framework-menu-system-notifications-remediation.ts +13 -24
- package/scripts/framework-menu-system-notifications-text.ts +1 -7
- package/scripts/framework-menu.ts +3 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
- package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
- package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
- package/scripts/pumuki-full-surface-smoke.ts +346 -0
- package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
- package/integrations/evidence/trackingContract.ts +0 -150
- package/integrations/gate/governanceActionCatalog.ts +0 -275
- package/integrations/lifecycle/bootstrapManifest.ts +0 -248
- package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
- package/integrations/lifecycle/governanceNextAction.ts +0 -164
- package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -613
- package/integrations/mcp/alignedPlatformGate.ts +0 -232
- package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
- package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +0 -200
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
} from './framework-menu-system-notifications-text';
|
|
6
6
|
|
|
7
7
|
const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
8
|
-
EVIDENCE_GATE_BLOCKED: 'El gate de evidencia de IA está bloqueado.',
|
|
9
8
|
EVIDENCE_MISSING: 'Falta evidencia para validar este paso.',
|
|
10
9
|
EVIDENCE_INVALID: 'La evidencia actual es inválida.',
|
|
11
10
|
EVIDENCE_CHAIN_INVALID: 'La cadena de evidencia no es válida.',
|
|
@@ -19,14 +18,6 @@ const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
19
18
|
OPENSPEC_MISSING: 'OpenSpec no está instalado en este repositorio.',
|
|
20
19
|
MCP_ENTERPRISE_RECEIPT_MISSING: 'Falta el recibo enterprise de MCP.',
|
|
21
20
|
BACKEND_AVOID_EXPLICIT_ANY: 'Se detectó uso de "any" explícito en backend.',
|
|
22
|
-
FRONTEND_NO_CONSOLE_LOG: 'Se detectó uso de "console.log" en frontend.',
|
|
23
|
-
BACKEND_NO_CONSOLE_LOG: 'Se detectó uso de "console.log" en backend.',
|
|
24
|
-
GIT_ATOMICITY_TOO_MANY_SCOPES: 'Se han cambiado demasiados scopes en el mismo commit.',
|
|
25
|
-
SOLID_HEURISTIC: 'Se detectó una violación estructural en el cambio actual.',
|
|
26
|
-
TRACKING_CANONICAL_SOURCE_CONFLICT:
|
|
27
|
-
'Hay varias fuentes canónicas de seguimiento declaradas.',
|
|
28
|
-
TRACKING_CANONICAL_IN_PROGRESS_INVALID:
|
|
29
|
-
'El archivo canónico de seguimiento debe tener exactamente una tarea en construcción.',
|
|
30
21
|
};
|
|
31
22
|
|
|
32
23
|
const ENGLISH_CAUSE_HINTS = [
|
|
@@ -65,27 +56,12 @@ const toKnownSpanishCauseFromMessage = (message: string): string | null => {
|
|
|
65
56
|
if (normalized.includes('avoid explicit any')) {
|
|
66
57
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE.BACKEND_AVOID_EXPLICIT_ANY;
|
|
67
58
|
}
|
|
68
|
-
if (normalized.includes('console.log usage is not allowed in frontend code')) {
|
|
69
|
-
return BLOCKED_CAUSE_SUMMARY_BY_CODE.FRONTEND_NO_CONSOLE_LOG;
|
|
70
|
-
}
|
|
71
|
-
if (normalized.includes('console.log usage is not allowed in backend code')) {
|
|
72
|
-
return BLOCKED_CAUSE_SUMMARY_BY_CODE.BACKEND_NO_CONSOLE_LOG;
|
|
73
|
-
}
|
|
74
59
|
if (normalized.includes('evidence is stale')) {
|
|
75
60
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE.EVIDENCE_STALE;
|
|
76
61
|
}
|
|
77
|
-
if (normalized.includes('evidence ai gate status is blocked')) {
|
|
78
|
-
return BLOCKED_CAUSE_SUMMARY_BY_CODE.EVIDENCE_GATE_BLOCKED;
|
|
79
|
-
}
|
|
80
62
|
if (normalized.includes('no upstream tracking reference')) {
|
|
81
63
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE.PRE_PUSH_UPSTREAM_MISSING;
|
|
82
64
|
}
|
|
83
|
-
if (normalized.includes('too many scopes changed') || normalized.includes('atomicity budget exceeded')) {
|
|
84
|
-
return BLOCKED_CAUSE_SUMMARY_BY_CODE.GIT_ATOMICITY_TOO_MANY_SCOPES;
|
|
85
|
-
}
|
|
86
|
-
if (normalized.includes('heuristic violation')) {
|
|
87
|
-
return BLOCKED_CAUSE_SUMMARY_BY_CODE.SOLID_HEURISTIC;
|
|
88
|
-
}
|
|
89
65
|
return null;
|
|
90
66
|
};
|
|
91
67
|
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
export const SWIFT_BLOCKED_DIALOG_SOURCE = String.raw`import AppKit
|
|
2
2
|
import Foundation
|
|
3
3
|
|
|
4
|
-
final class KeyableFloatingPanel: NSPanel {
|
|
5
|
-
override var canBecomeKey: Bool { true }
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
struct DialogConfig {
|
|
9
5
|
let title: String
|
|
10
6
|
let cause: String
|
|
@@ -12,7 +8,6 @@ struct DialogConfig {
|
|
|
12
8
|
let disableButton: String
|
|
13
9
|
let muteButton: String
|
|
14
10
|
let keepButton: String
|
|
15
|
-
let timeoutSeconds: Double
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
func parseArguments() -> DialogConfig {
|
|
@@ -23,74 +18,18 @@ func parseArguments() -> DialogConfig {
|
|
|
23
18
|
}
|
|
24
19
|
return args[index + 1]
|
|
25
20
|
}
|
|
26
|
-
let timeoutRaw = read("--timeout-seconds", fallback: "15")
|
|
27
|
-
let timeout = Double(timeoutRaw) ?? 15
|
|
28
21
|
return DialogConfig(
|
|
29
22
|
title: read("--title", fallback: "Pumuki bloqueado"),
|
|
30
23
|
cause: read("--cause", fallback: "Bloqueo detectado."),
|
|
31
24
|
remediation: read("--remediation", fallback: "Corrige el bloqueo y vuelve a ejecutar."),
|
|
32
25
|
disableButton: read("--disable-button", fallback: "Desactivar"),
|
|
33
26
|
muteButton: read("--mute-button", fallback: "Silenciar 30 min"),
|
|
34
|
-
keepButton: read("--keep-button", fallback: "Mantener activas")
|
|
35
|
-
timeoutSeconds: max(5, timeout)
|
|
27
|
+
keepButton: read("--keep-button", fallback: "Mantener activas")
|
|
36
28
|
)
|
|
37
29
|
}
|
|
38
30
|
|
|
39
|
-
final class
|
|
31
|
+
final class DialogAppDelegate: NSObject, NSApplicationDelegate {
|
|
40
32
|
private let config: DialogConfig
|
|
41
|
-
private var window: NSWindow?
|
|
42
|
-
private var chosenButton: String?
|
|
43
|
-
|
|
44
|
-
private func preferredWidth() -> CGFloat {
|
|
45
|
-
let longest = max(config.title.count, max(config.cause.count, config.remediation.count))
|
|
46
|
-
let estimated = CGFloat(longest) * 4.9 + 170
|
|
47
|
-
let visibleFrame = targetVisibleFrame()
|
|
48
|
-
let maxAllowed = max(360, visibleFrame.width - 40)
|
|
49
|
-
return min(max(360, estimated), min(620, maxAllowed))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private func targetVisibleFrame() -> NSRect {
|
|
53
|
-
let mouse = NSEvent.mouseLocation
|
|
54
|
-
if let screen = NSScreen.screens.first(where: { NSMouseInRect(mouse, $0.frame, false) }) {
|
|
55
|
-
return screen.visibleFrame
|
|
56
|
-
}
|
|
57
|
-
if let main = NSScreen.main {
|
|
58
|
-
return main.visibleFrame
|
|
59
|
-
}
|
|
60
|
-
if let first = NSScreen.screens.first {
|
|
61
|
-
return first.visibleFrame
|
|
62
|
-
}
|
|
63
|
-
return NSRect(x: 0, y: 0, width: 1440, height: 900)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private func pinWindowToBottomRight() {
|
|
67
|
-
guard let panel = window else {
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
let width = panel.frame.width
|
|
71
|
-
let height = panel.frame.height
|
|
72
|
-
let margin: CGFloat = 20
|
|
73
|
-
let visibleFrame = targetVisibleFrame()
|
|
74
|
-
let target = NSRect(
|
|
75
|
-
x: visibleFrame.maxX - width - margin,
|
|
76
|
-
y: visibleFrame.minY + margin,
|
|
77
|
-
width: width,
|
|
78
|
-
height: height
|
|
79
|
-
)
|
|
80
|
-
panel.setFrame(target, display: true)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private func resizeWindowToContent(root: NSStackView, contentView: NSView) {
|
|
84
|
-
guard let panel = window else {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
contentView.layoutSubtreeIfNeeded()
|
|
88
|
-
let fitting = root.fittingSize
|
|
89
|
-
let visibleFrame = targetVisibleFrame()
|
|
90
|
-
let width = min(max(panel.frame.width, fitting.width + 30), min(620, visibleFrame.width - 40))
|
|
91
|
-
let height = min(max(140, fitting.height + 30), max(180, visibleFrame.height - 40))
|
|
92
|
-
panel.setContentSize(NSSize(width: width, height: height))
|
|
93
|
-
}
|
|
94
33
|
|
|
95
34
|
init(config: DialogConfig) {
|
|
96
35
|
self.config = config
|
|
@@ -98,149 +37,30 @@ final class DialogController: NSObject, NSApplicationDelegate, NSWindowDelegate
|
|
|
98
37
|
}
|
|
99
38
|
|
|
100
39
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
101
|
-
showWindow()
|
|
102
|
-
startTimeout()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private func showWindow() {
|
|
106
|
-
let width = preferredWidth()
|
|
107
|
-
let height: CGFloat = 170
|
|
108
|
-
let margin: CGFloat = 20
|
|
109
|
-
let screenFrame = targetVisibleFrame()
|
|
110
|
-
let origin = NSPoint(
|
|
111
|
-
x: screenFrame.maxX - width - margin,
|
|
112
|
-
y: screenFrame.minY + margin
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
let panel = KeyableFloatingPanel(
|
|
116
|
-
contentRect: NSRect(x: origin.x, y: origin.y, width: width, height: height),
|
|
117
|
-
styleMask: [.titled, .closable, .fullSizeContentView],
|
|
118
|
-
backing: .buffered,
|
|
119
|
-
defer: false
|
|
120
|
-
)
|
|
121
|
-
panel.becomesKeyOnlyIfNeeded = false
|
|
122
|
-
panel.level = .floating
|
|
123
|
-
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
124
|
-
panel.titleVisibility = .hidden
|
|
125
|
-
panel.titlebarAppearsTransparent = true
|
|
126
|
-
panel.isMovable = true
|
|
127
|
-
panel.isReleasedWhenClosed = false
|
|
128
|
-
panel.delegate = self
|
|
129
|
-
panel.hidesOnDeactivate = false
|
|
130
|
-
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
131
|
-
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
132
|
-
|
|
133
|
-
let root = NSStackView()
|
|
134
|
-
root.orientation = .vertical
|
|
135
|
-
root.alignment = .leading
|
|
136
|
-
root.spacing = 12
|
|
137
|
-
root.translatesAutoresizingMaskIntoConstraints = false
|
|
138
|
-
|
|
139
|
-
let titleField = NSTextField(labelWithString: config.title)
|
|
140
|
-
titleField.font = NSFont.boldSystemFont(ofSize: 15)
|
|
141
|
-
titleField.lineBreakMode = .byWordWrapping
|
|
142
|
-
titleField.maximumNumberOfLines = 0
|
|
143
|
-
titleField.preferredMaxLayoutWidth = width - 48
|
|
144
|
-
titleField.cell?.lineBreakMode = .byWordWrapping
|
|
145
|
-
titleField.cell?.usesSingleLineMode = false
|
|
146
|
-
titleField.cell?.wraps = true
|
|
147
|
-
|
|
148
|
-
let causeField = NSTextField(wrappingLabelWithString: "Causa: \(config.cause)")
|
|
149
|
-
causeField.font = NSFont.systemFont(ofSize: 12)
|
|
150
|
-
causeField.lineBreakMode = .byWordWrapping
|
|
151
|
-
causeField.maximumNumberOfLines = 0
|
|
152
|
-
causeField.preferredMaxLayoutWidth = width - 48
|
|
153
|
-
causeField.cell?.lineBreakMode = .byWordWrapping
|
|
154
|
-
causeField.cell?.usesSingleLineMode = false
|
|
155
|
-
causeField.cell?.wraps = true
|
|
156
|
-
|
|
157
|
-
let remediationField = NSTextField(wrappingLabelWithString: "Solución: \(config.remediation)")
|
|
158
|
-
remediationField.font = NSFont.systemFont(ofSize: 12)
|
|
159
|
-
remediationField.lineBreakMode = .byWordWrapping
|
|
160
|
-
remediationField.maximumNumberOfLines = 0
|
|
161
|
-
remediationField.preferredMaxLayoutWidth = width - 48
|
|
162
|
-
remediationField.cell?.lineBreakMode = .byWordWrapping
|
|
163
|
-
remediationField.cell?.usesSingleLineMode = false
|
|
164
|
-
remediationField.cell?.wraps = true
|
|
165
|
-
|
|
166
|
-
let buttons = NSStackView()
|
|
167
|
-
buttons.orientation = .horizontal
|
|
168
|
-
buttons.alignment = .centerY
|
|
169
|
-
buttons.spacing = 10
|
|
170
|
-
|
|
171
|
-
let disableButton = NSButton(title: config.disableButton, target: self, action: #selector(disablePressed))
|
|
172
|
-
disableButton.bezelStyle = .rounded
|
|
173
|
-
let muteButton = NSButton(title: config.muteButton, target: self, action: #selector(mutePressed))
|
|
174
|
-
muteButton.bezelStyle = .rounded
|
|
175
|
-
let keepButton = NSButton(title: config.keepButton, target: self, action: #selector(keepPressed))
|
|
176
|
-
keepButton.bezelStyle = .rounded
|
|
177
|
-
keepButton.keyEquivalent = "\r"
|
|
178
|
-
|
|
179
|
-
buttons.addArrangedSubview(disableButton)
|
|
180
|
-
buttons.addArrangedSubview(muteButton)
|
|
181
|
-
buttons.addArrangedSubview(keepButton)
|
|
182
|
-
|
|
183
|
-
root.addArrangedSubview(titleField)
|
|
184
|
-
root.addArrangedSubview(causeField)
|
|
185
|
-
root.addArrangedSubview(remediationField)
|
|
186
|
-
root.addArrangedSubview(buttons)
|
|
187
|
-
|
|
188
|
-
panel.contentView = NSView()
|
|
189
|
-
guard let contentView = panel.contentView else {
|
|
190
|
-
finish(with: config.keepButton)
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
contentView.addSubview(root)
|
|
194
|
-
NSLayoutConstraint.activate([
|
|
195
|
-
root.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 18),
|
|
196
|
-
root.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -18),
|
|
197
|
-
root.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18),
|
|
198
|
-
root.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -18),
|
|
199
|
-
])
|
|
200
|
-
|
|
201
|
-
self.window = panel
|
|
202
|
-
resizeWindowToContent(root: root, contentView: contentView)
|
|
203
|
-
pinWindowToBottomRight()
|
|
204
|
-
panel.makeKeyAndOrderFront(nil)
|
|
205
|
-
panel.orderFrontRegardless()
|
|
206
40
|
NSApp.activate(ignoringOtherApps: true)
|
|
207
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
208
|
-
guard let self else {
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
self.resizeWindowToContent(root: root, contentView: contentView)
|
|
212
|
-
self.pinWindowToBottomRight()
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
41
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
42
|
+
let alert = NSAlert()
|
|
43
|
+
alert.messageText = config.title
|
|
44
|
+
alert.informativeText = "Causa: \(config.cause)\n\nSolución: \(config.remediation)"
|
|
45
|
+
alert.alertStyle = .critical
|
|
46
|
+
alert.addButton(withTitle: config.keepButton)
|
|
47
|
+
alert.addButton(withTitle: config.muteButton)
|
|
48
|
+
alert.addButton(withTitle: config.disableButton)
|
|
49
|
+
|
|
50
|
+
let response = alert.runModal()
|
|
51
|
+
let choice: String
|
|
52
|
+
switch response {
|
|
53
|
+
case .alertFirstButtonReturn:
|
|
54
|
+
choice = config.keepButton
|
|
55
|
+
case .alertSecondButtonReturn:
|
|
56
|
+
choice = config.muteButton
|
|
57
|
+
case .alertThirdButtonReturn:
|
|
58
|
+
choice = config.disableButton
|
|
59
|
+
default:
|
|
60
|
+
choice = config.keepButton
|
|
219
61
|
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
func windowWillClose(_ notification: Notification) {
|
|
223
|
-
finish(with: config.keepButton)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
@objc private func disablePressed() {
|
|
227
|
-
finish(with: config.disableButton)
|
|
228
|
-
}
|
|
229
62
|
|
|
230
|
-
|
|
231
|
-
finish(with: config.muteButton)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
@objc private func keepPressed() {
|
|
235
|
-
finish(with: config.keepButton)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private func finish(with button: String) {
|
|
239
|
-
guard chosenButton == nil else {
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
chosenButton = button
|
|
243
|
-
FileHandle.standardOutput.write(Data("button returned:\(button)\n".utf8))
|
|
63
|
+
FileHandle.standardOutput.write(Data("button returned:\(choice)\n".utf8))
|
|
244
64
|
NSApp.terminate(nil)
|
|
245
65
|
}
|
|
246
66
|
|
|
@@ -252,7 +72,7 @@ final class DialogController: NSObject, NSApplicationDelegate, NSWindowDelegate
|
|
|
252
72
|
let config = parseArguments()
|
|
253
73
|
let app = NSApplication.shared
|
|
254
74
|
app.setActivationPolicy(.accessory)
|
|
255
|
-
let
|
|
256
|
-
app.delegate =
|
|
75
|
+
let delegate = DialogAppDelegate(config: config)
|
|
76
|
+
app.delegate = delegate
|
|
257
77
|
app.run()
|
|
258
78
|
`;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type SystemNotificationCommandRunnerWithOutput,
|
|
6
6
|
type SystemNotificationsConfig,
|
|
7
7
|
} from './framework-menu-system-notifications-types';
|
|
8
|
+
import { isTruthyEnvValue } from './framework-menu-system-notifications-env';
|
|
8
9
|
import { resolveBlockedDialogEnabled } from './framework-menu-system-notifications-macos-dialog-enabled';
|
|
9
10
|
import { emitMacOsBannerStage } from './framework-menu-system-notifications-macos-banner-stage';
|
|
10
11
|
import { emitMacOsBlockedDialogStage } from './framework-menu-system-notifications-macos-blocked-stage';
|
|
@@ -19,6 +20,9 @@ const shouldSkipMacOsBannerForInteractiveBlockedDialog = (params: {
|
|
|
19
20
|
config: SystemNotificationsConfig;
|
|
20
21
|
env: NodeJS.ProcessEnv;
|
|
21
22
|
}): boolean => {
|
|
23
|
+
if (!isTruthyEnvValue(params.env.PUMUKI_MACOS_GATE_BLOCKED_BANNER_DEDUPE)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
22
26
|
if (params.event.kind !== 'gate.blocked') {
|
|
23
27
|
return false;
|
|
24
28
|
}
|
|
@@ -22,7 +22,7 @@ export const buildGateBlockedPayload = (
|
|
|
22
22
|
resolveBlockedCauseSummary(event, causeCode),
|
|
23
23
|
72
|
|
24
24
|
);
|
|
25
|
-
const remediation = resolveBlockedRemediation(event, causeCode
|
|
25
|
+
const remediation = resolveBlockedRemediation(event, causeCode);
|
|
26
26
|
return {
|
|
27
27
|
title: '🔴 Pumuki bloqueado',
|
|
28
28
|
subtitle: `${projectPrefix}${event.stage} · ${causeSummary}`,
|
|
@@ -4,30 +4,22 @@ import {
|
|
|
4
4
|
truncateNotificationText,
|
|
5
5
|
} from './framework-menu-system-notifications-text';
|
|
6
6
|
|
|
7
|
-
type BlockedRemediationVariant = 'banner' | 'dialog';
|
|
8
|
-
|
|
9
7
|
const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
EVIDENCE_INVALID: 'Regenera la evidencia de esta iteración y repite la validación.',
|
|
8
|
+
EVIDENCE_MISSING: 'Genera evidencia del slice actual y vuelve a validar el gate de esta fase.',
|
|
9
|
+
EVIDENCE_INVALID: 'Regenera la evidencia de la iteración y repite la validación en el mismo stage.',
|
|
13
10
|
EVIDENCE_CHAIN_INVALID: 'Regenera la evidencia para restaurar la cadena de integridad y vuelve a validar.',
|
|
14
|
-
EVIDENCE_STALE: '
|
|
15
|
-
EVIDENCE_BRANCH_MISMATCH: 'Regenera
|
|
16
|
-
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera
|
|
17
|
-
PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin <branch>` y
|
|
18
|
-
SDD_SESSION_MISSING: 'Abre
|
|
19
|
-
SDD_SESSION_INVALID: 'Refresca la sesión SDD
|
|
20
|
-
OPENSPEC_MISSING: 'Instala OpenSpec en
|
|
21
|
-
MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a
|
|
22
|
-
BACKEND_AVOID_EXPLICIT_ANY: '
|
|
23
|
-
GIT_ATOMICITY_TOO_MANY_SCOPES: 'Divide el cambio por scope o en commits más pequeños y vuelve a ejecutar el gate.',
|
|
24
|
-
SOLID_HEURISTIC: 'Corrige la violación detectada y vuelve a ejecutar el gate.',
|
|
11
|
+
EVIDENCE_STALE: 'Ejecuta una auditoría completa de evidencia y vuelve a validar PRE_WRITE/PRE_PUSH. Si persiste, refresca la sesión SDD y reintenta.',
|
|
12
|
+
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en esta rama y vuelve a ejecutar la validación para sincronizar branch y snapshot.',
|
|
13
|
+
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este repositorio y relanza la validación del gate.',
|
|
14
|
+
PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin <branch>` y vuelve a ejecutar PRE_PUSH.',
|
|
15
|
+
SDD_SESSION_MISSING: 'Abre sesión SDD del change activo y repite la validación de la fase actual.',
|
|
16
|
+
SDD_SESSION_INVALID: 'Refresca la sesión SDD (open/refresh) y vuelve a validar en el mismo stage.',
|
|
17
|
+
OPENSPEC_MISSING: 'Instala OpenSpec en el repositorio y relanza la validación del gate.',
|
|
18
|
+
MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a ejecutar la validación.',
|
|
19
|
+
BACKEND_AVOID_EXPLICIT_ANY: 'Reemplaza `any` por tipos concretos en backend y vuelve a lanzar el gate para confirmar el fix.',
|
|
25
20
|
};
|
|
26
21
|
|
|
27
|
-
const
|
|
28
|
-
banner: 120,
|
|
29
|
-
dialog: 220,
|
|
30
|
-
};
|
|
22
|
+
const BLOCKED_REMEDIATION_MAX_LENGTH = 220;
|
|
31
23
|
|
|
32
24
|
const GENERIC_BLOCKED_REMEDIATION =
|
|
33
25
|
'Corrige el bloqueo indicado y vuelve a ejecutar el comando.';
|
|
@@ -95,10 +87,7 @@ const toKnownSpanishRemediationFromMessage = (message: string, causeCode: string
|
|
|
95
87
|
|
|
96
88
|
export const resolveBlockedRemediation = (
|
|
97
89
|
event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>,
|
|
98
|
-
causeCode: string
|
|
99
|
-
options?: {
|
|
100
|
-
variant?: BlockedRemediationVariant;
|
|
101
|
-
}
|
|
90
|
+
causeCode: string
|
|
102
91
|
): string => {
|
|
103
92
|
const variant = options?.variant ?? 'dialog';
|
|
104
93
|
const maxLength = BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT[variant];
|
|
@@ -7,13 +7,7 @@ export const truncateNotificationText = (value: string, maxLength: number): stri
|
|
|
7
7
|
if (value.length <= maxLength) {
|
|
8
8
|
return value;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
const trimmed = value.slice(0, limit).trimEnd();
|
|
12
|
-
const boundary = trimmed.lastIndexOf(' ');
|
|
13
|
-
const shortened = boundary >= Math.floor(limit * 0.6)
|
|
14
|
-
? trimmed.slice(0, boundary).trimEnd()
|
|
15
|
-
: trimmed;
|
|
16
|
-
return `${shortened}…`;
|
|
10
|
+
return `${value.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
17
11
|
};
|
|
18
12
|
|
|
19
13
|
export const resolveProjectLabel = (params: {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runWorkingTreeGateSilent,
|
|
17
17
|
runRepoAndStagedPrePushGateSilent,
|
|
18
18
|
runWorkingTreePrePushGateSilent,
|
|
19
|
+
runUnstagedGateSilent,
|
|
19
20
|
} from './framework-menu-gate-lib';
|
|
20
21
|
import { createFrameworkMenuPrompts } from './framework-menu-prompts';
|
|
21
22
|
import { resolveDefaultRangeFrom } from './framework-menu-runners';
|
|
@@ -62,7 +63,9 @@ const menu = async (): Promise<void> => {
|
|
|
62
63
|
runRepoGate: runRepoGateSilent,
|
|
63
64
|
runRepoAndStagedGate: runRepoAndStagedPrePushGateSilent,
|
|
64
65
|
runStagedGate: runStagedGateSilent,
|
|
66
|
+
runUnstagedGate: runUnstagedGateSilent,
|
|
65
67
|
runWorkingTreeGate: runWorkingTreePrePushGateSilent,
|
|
68
|
+
runWorkingTreePreCommitGate: runWorkingTreeGateSilent,
|
|
66
69
|
write: (text) => {
|
|
67
70
|
output.write(text);
|
|
68
71
|
},
|
|
@@ -73,7 +76,6 @@ const menu = async (): Promise<void> => {
|
|
|
73
76
|
consumerRuntime.printMenu();
|
|
74
77
|
} else {
|
|
75
78
|
const currentSummary = consumerRuntime.readCurrentSummary();
|
|
76
|
-
const lastPreflight = consumerRuntime.readLastPreflight();
|
|
77
79
|
if (!isMenuUiV2Enabled()) {
|
|
78
80
|
output.write(
|
|
79
81
|
`\n${formatAdvancedMenuClassicView(advancedActions, {
|
|
@@ -85,7 +87,6 @@ const menu = async (): Promise<void> => {
|
|
|
85
87
|
output.write(
|
|
86
88
|
`\n${formatAdvancedMenuView(advancedActions, {
|
|
87
89
|
evidenceSummary: currentSummary ?? undefined,
|
|
88
|
-
preflight: lastPreflight ?? undefined,
|
|
89
90
|
})}\n`
|
|
90
91
|
);
|
|
91
92
|
} catch {
|
|
@@ -39,17 +39,8 @@ export const configureRemoteAndFeatureBranch = (
|
|
|
39
39
|
workspace.tmpRoot
|
|
40
40
|
);
|
|
41
41
|
runGitStep(workspace, ['remote', 'add', 'origin', workspace.bareRemote], 'git remote add origin');
|
|
42
|
+
runGitStep(workspace, ['push', '-u', 'origin', 'main'], 'git push origin main');
|
|
42
43
|
runGitStep(workspace, ['checkout', '-b', 'feature/package-smoke'], 'git checkout feature branch');
|
|
43
|
-
runGitStep(
|
|
44
|
-
workspace,
|
|
45
|
-
['push', '-u', 'origin', 'feature/package-smoke'],
|
|
46
|
-
'git push origin feature branch'
|
|
47
|
-
);
|
|
48
|
-
runGitStep(
|
|
49
|
-
workspace,
|
|
50
|
-
['push', 'origin', 'HEAD:refs/heads/main'],
|
|
51
|
-
'git push origin main from feature branch'
|
|
52
|
-
);
|
|
53
44
|
runGitStep(
|
|
54
45
|
workspace,
|
|
55
46
|
['branch', '--set-upstream-to=origin/main', 'feature/package-smoke'],
|
|
@@ -15,11 +15,10 @@ import packageJson from '../package.json';
|
|
|
15
15
|
const runNpmStep = (
|
|
16
16
|
workspace: SmokeWorkspace,
|
|
17
17
|
args: string[],
|
|
18
|
-
context: string
|
|
19
|
-
env?: NodeJS.ProcessEnv
|
|
18
|
+
context: string
|
|
20
19
|
): void => {
|
|
21
20
|
assertSuccess(
|
|
22
|
-
runCommand({ cwd: workspace.consumerRepo, executable: 'npm', args
|
|
21
|
+
runCommand({ cwd: workspace.consumerRepo, executable: 'npm', args }),
|
|
23
22
|
context
|
|
24
23
|
);
|
|
25
24
|
};
|
|
@@ -33,14 +32,7 @@ export const installTarballIntoConsumerRepo = (
|
|
|
33
32
|
'node_modules/\n.ai_evidence.json\n',
|
|
34
33
|
'utf8'
|
|
35
34
|
);
|
|
36
|
-
runNpmStep(
|
|
37
|
-
workspace,
|
|
38
|
-
['install', workspace.tarballPath ?? ''],
|
|
39
|
-
'npm install <tarball>',
|
|
40
|
-
{
|
|
41
|
-
PUMUKI_SKIP_POSTINSTALL: '1',
|
|
42
|
-
}
|
|
43
|
-
);
|
|
35
|
+
runNpmStep(workspace, ['install', workspace.tarballPath ?? ''], 'npm install <tarball>');
|
|
44
36
|
};
|
|
45
37
|
|
|
46
38
|
export const verifyInstalledPackageCanBeRequired = (
|
|
@@ -59,40 +51,10 @@ export const verifyInstalledPackageCanBeRequired = (
|
|
|
59
51
|
export const verifyInstalledPumukiBinaryVersion = (
|
|
60
52
|
workspace: SmokeWorkspace
|
|
61
53
|
): void => {
|
|
62
|
-
const hasInstalledStatusVersion = (
|
|
63
|
-
result: ReturnType<typeof runCommand>,
|
|
64
|
-
): boolean => {
|
|
65
|
-
let parsed: unknown;
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(result.stdout);
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const packageVersion =
|
|
73
|
-
typeof parsed === 'object' && parsed !== null && 'packageVersion' in parsed
|
|
74
|
-
? (parsed as { packageVersion?: unknown }).packageVersion
|
|
75
|
-
: null;
|
|
76
|
-
const effectiveVersion =
|
|
77
|
-
typeof parsed === 'object'
|
|
78
|
-
&& parsed !== null
|
|
79
|
-
&& 'version' in parsed
|
|
80
|
-
&& typeof (parsed as { version?: unknown }).version === 'object'
|
|
81
|
-
&& (parsed as { version: { effective?: unknown } }).version !== null
|
|
82
|
-
? (parsed as { version: { effective?: unknown } }).version.effective
|
|
83
|
-
: null;
|
|
84
|
-
|
|
85
|
-
return packageVersion === packageJson.version || effectiveVersion === packageJson.version;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
54
|
const assertInstalledStatusVersion = (
|
|
89
55
|
result: ReturnType<typeof runCommand>,
|
|
90
56
|
context: string
|
|
91
57
|
): void => {
|
|
92
|
-
if (hasInstalledStatusVersion(result)) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
58
|
let parsed: unknown;
|
|
97
59
|
try {
|
|
98
60
|
parsed = JSON.parse(result.stdout);
|
|
@@ -113,9 +75,11 @@ export const verifyInstalledPumukiBinaryVersion = (
|
|
|
113
75
|
? (parsed as { version: { effective?: unknown } }).version.effective
|
|
114
76
|
: null;
|
|
115
77
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
78
|
+
if (packageVersion !== packageJson.version && effectiveVersion !== packageJson.version) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${context} reported unexpected version (packageVersion=${String(packageVersion)}, effectiveVersion=${String(effectiveVersion)}, expected=${packageJson.version})`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
119
83
|
};
|
|
120
84
|
|
|
121
85
|
const noInstallVersionCheck = runCommand({
|
|
@@ -129,8 +93,7 @@ export const verifyInstalledPumukiBinaryVersion = (
|
|
|
129
93
|
noInstallVersionCheck.exitCode === 0
|
|
130
94
|
&& !/Cannot find module|ERR_MODULE_NOT_FOUND|failed to resolve tsx runtime/.test(
|
|
131
95
|
noInstallVersionCheck.combined
|
|
132
|
-
)
|
|
133
|
-
&& hasInstalledStatusVersion(noInstallVersionCheck);
|
|
96
|
+
);
|
|
134
97
|
if (noInstallPassed) {
|
|
135
98
|
assertNoFatalOutput(noInstallVersionCheck, 'pumuki status --json smoke');
|
|
136
99
|
assertInstalledStatusVersion(noInstallVersionCheck, 'pumuki status --json smoke');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
export type SmokeBinStrategy = 'source' | 'installed';
|
|
5
|
+
|
|
6
|
+
export const resolveBinStrategy = (raw: string | undefined): SmokeBinStrategy => {
|
|
7
|
+
const normalized = (raw ?? 'source').trim().toLowerCase();
|
|
8
|
+
if (normalized === 'installed' || normalized === 'consumer') {
|
|
9
|
+
return 'installed';
|
|
10
|
+
}
|
|
11
|
+
return 'source';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SmokeLayout = {
|
|
15
|
+
pumukiPackageRoot: string;
|
|
16
|
+
smokeCwd: string;
|
|
17
|
+
binStrategy: SmokeBinStrategy;
|
|
18
|
+
binRoot: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const resolveSmokeLayout = (params: {
|
|
22
|
+
scriptFileUrl: string;
|
|
23
|
+
env: NodeJS.ProcessEnv;
|
|
24
|
+
}): SmokeLayout => {
|
|
25
|
+
const pumukiPackageRoot = join(dirname(fileURLToPath(params.scriptFileUrl)), '..');
|
|
26
|
+
const smokeCwd =
|
|
27
|
+
(params.env.PUMUKI_SMOKE_REPO_ROOT ?? pumukiPackageRoot).trim() || pumukiPackageRoot;
|
|
28
|
+
const binStrategy = resolveBinStrategy(params.env.PUMUKI_SMOKE_BIN_STRATEGY);
|
|
29
|
+
const binRoot =
|
|
30
|
+
binStrategy === 'installed'
|
|
31
|
+
? join(smokeCwd, 'node_modules', 'pumuki')
|
|
32
|
+
: pumukiPackageRoot;
|
|
33
|
+
return { pumukiPackageRoot, smokeCwd, binStrategy, binRoot };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const installedBinMarkerPath = (layout: SmokeLayout): string =>
|
|
37
|
+
join(layout.binRoot, 'bin', 'pumuki.js');
|