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.
Files changed (96) hide show
  1. package/AGENTS.md +269 -0
  2. package/CHANGELOG.md +697 -0
  3. package/README.md +4 -2
  4. package/VERSION +1 -1
  5. package/docs/README.md +13 -9
  6. package/docs/operations/RELEASE_NOTES.md +12 -76
  7. package/docs/product/HOW_IT_WORKS.md +6 -0
  8. package/docs/product/INSTALLATION.md +1 -1
  9. package/docs/product/USAGE.md +41 -4
  10. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +118 -0
  11. package/docs/validation/README.md +6 -3
  12. package/integrations/config/skillsCustomRules.ts +18 -99
  13. package/integrations/evidence/buildEvidence.ts +0 -24
  14. package/integrations/evidence/repoState.ts +0 -3
  15. package/integrations/evidence/schema.ts +0 -18
  16. package/integrations/evidence/writeEvidence.ts +0 -24
  17. package/integrations/gate/evaluateAiGate.ts +15 -232
  18. package/integrations/gate/remediationCatalog.ts +0 -8
  19. package/integrations/git/GitService.ts +44 -5
  20. package/integrations/git/aiGateRepoPolicyFindings.ts +0 -4
  21. package/integrations/git/runPlatformGate.ts +1 -9
  22. package/integrations/git/runPlatformGateFacts.ts +19 -1
  23. package/integrations/git/runPlatformGateOutput.ts +27 -36
  24. package/integrations/lifecycle/adapter.templates.json +7 -13
  25. package/integrations/lifecycle/adapter.ts +0 -24
  26. package/integrations/lifecycle/artifacts.ts +1 -6
  27. package/integrations/lifecycle/audit.ts +101 -0
  28. package/integrations/lifecycle/cli.ts +110 -70
  29. package/integrations/lifecycle/cliSdd.ts +13 -8
  30. package/integrations/lifecycle/doctor.ts +16 -48
  31. package/integrations/lifecycle/hookManager.ts +0 -77
  32. package/integrations/lifecycle/index.ts +2 -0
  33. package/integrations/lifecycle/install.ts +0 -21
  34. package/integrations/lifecycle/npmService.ts +3 -155
  35. package/integrations/lifecycle/policyValidationSnapshot.ts +8 -2
  36. package/integrations/lifecycle/preWriteAutomation.ts +7 -77
  37. package/integrations/lifecycle/state.ts +1 -8
  38. package/integrations/lifecycle/status.ts +2 -29
  39. package/integrations/mcp/aiGateCheck.ts +26 -206
  40. package/integrations/mcp/autoExecuteAiStart.ts +87 -94
  41. package/integrations/mcp/enterpriseServer.ts +7 -23
  42. package/integrations/mcp/enterpriseStdioServer.cli.ts +4 -31
  43. package/integrations/mcp/preFlightCheck.ts +5 -51
  44. package/integrations/platform/detectPlatforms.ts +37 -0
  45. package/integrations/policy/experimentalFeatures.ts +1 -1
  46. package/integrations/sdd/evidenceScaffold.ts +2 -109
  47. package/package.json +10 -2
  48. package/scripts/check-tracking-single-active.sh +1 -1
  49. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
  50. package/scripts/consumer-postinstall-resolve-args.cjs +44 -0
  51. package/scripts/consumer-postinstall.cjs +76 -21
  52. package/scripts/framework-menu-advanced-view-lib.ts +0 -15
  53. package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
  54. package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
  55. package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
  56. package/scripts/framework-menu-consumer-preflight-run.ts +0 -23
  57. package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
  58. package/scripts/framework-menu-consumer-runtime-actions.ts +87 -17
  59. package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
  60. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
  61. package/scripts/framework-menu-consumer-runtime-lib.ts +2 -10
  62. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -18
  63. package/scripts/framework-menu-consumer-runtime-types.ts +3 -3
  64. package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
  65. package/scripts/framework-menu-evidence-summary-read.ts +57 -5
  66. package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
  67. package/scripts/framework-menu-evidence-summary-types.ts +7 -0
  68. package/scripts/framework-menu-gate-lib.ts +9 -0
  69. package/scripts/framework-menu-layout-data.ts +5 -0
  70. package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
  71. package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
  72. package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
  73. package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
  74. package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
  75. package/scripts/framework-menu-system-notifications-cause.ts +0 -24
  76. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +24 -204
  77. package/scripts/framework-menu-system-notifications-macos.ts +4 -0
  78. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
  79. package/scripts/framework-menu-system-notifications-remediation.ts +13 -24
  80. package/scripts/framework-menu-system-notifications-text.ts +1 -7
  81. package/scripts/framework-menu.ts +3 -2
  82. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
  83. package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
  84. package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
  85. package/scripts/pumuki-full-surface-smoke.ts +346 -0
  86. package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
  87. package/integrations/evidence/trackingContract.ts +0 -150
  88. package/integrations/gate/governanceActionCatalog.ts +0 -275
  89. package/integrations/lifecycle/bootstrapManifest.ts +0 -248
  90. package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
  91. package/integrations/lifecycle/governanceNextAction.ts +0 -164
  92. package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -613
  93. package/integrations/mcp/alignedPlatformGate.ts +0 -232
  94. package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
  95. package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
  96. 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 DialogController: NSObject, NSApplicationDelegate, NSWindowDelegate {
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
- private func startTimeout() {
217
- Timer.scheduledTimer(withTimeInterval: config.timeoutSeconds, repeats: false) { [weak self] _ in
218
- self?.finish(with: self?.config.keepButton ?? "Mantener activas")
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
- @objc private func mutePressed() {
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 controller = DialogController(config: config)
256
- app.delegate = controller
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, { variant: 'banner' });
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
- EVIDENCE_GATE_BLOCKED: 'Corrige las violaciones bloqueantes detectadas en la evidencia y vuelve a ejecutar el gate.',
11
- EVIDENCE_MISSING: 'Genera la evidencia del slice actual y vuelve a validar esta fase.',
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: 'Refresca la evidencia y vuelve a validar PRE_WRITE/PRE_PUSH.',
15
- EVIDENCE_BRANCH_MISMATCH: 'Regenera la evidencia en esta rama y repite la validación.',
16
- EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera la evidencia desde este repositorio y vuelve a validar.',
17
- PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin <branch>` y repite PRE_PUSH.',
18
- SDD_SESSION_MISSING: 'Abre la sesión SDD del change activo y repite la validación.',
19
- SDD_SESSION_INVALID: 'Refresca la sesión SDD activa y vuelve a validar esta fase.',
20
- OPENSPEC_MISSING: 'Instala OpenSpec en este repositorio y vuelve a validar el gate.',
21
- MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a validar.',
22
- BACKEND_AVOID_EXPLICIT_ANY: 'Sustituye `any` por tipos concretos en backend y relanza el gate.',
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 BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly<Record<BlockedRemediationVariant, number>> = {
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
- const limit = Math.max(1, maxLength - 1);
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, env }),
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
- throw new Error(
117
- `${context} reported unexpected version (packageVersion=${String(packageVersion)}, effectiveVersion=${String(effectiveVersion)}, expected=${packageJson.version})`
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');