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.
@@ -16,6 +16,16 @@ export type HookUninstallResult = {
16
16
  changedHooks: ReadonlyArray<PumukiManagedHook>;
17
17
  };
18
18
 
19
+ export type PumukiHooksDirectoryResolutionSource =
20
+ | 'git-rev-parse'
21
+ | 'git-config'
22
+ | 'default';
23
+
24
+ export type PumukiHooksDirectoryResolution = {
25
+ path: string;
26
+ source: PumukiHooksDirectoryResolutionSource;
27
+ };
28
+
19
29
  const HOOK_FILE_MODE = 0o755;
20
30
 
21
31
  const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null => {
@@ -34,8 +44,82 @@ const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null
34
44
  }
35
45
  };
36
46
 
47
+ const readCoreHooksPathFromGitConfig = (repoRoot: string): string | null => {
48
+ const configPath = join(repoRoot, '.git', 'config');
49
+ if (!existsSync(configPath)) {
50
+ return null;
51
+ }
52
+
53
+ let contents = '';
54
+ try {
55
+ contents = readFileSync(configPath, 'utf8');
56
+ } catch {
57
+ return null;
58
+ }
59
+
60
+ let inCoreSection = false;
61
+ for (const rawLine of contents.split(/\r?\n/)) {
62
+ const line = rawLine.trim();
63
+ if (line.length === 0 || line.startsWith(';') || line.startsWith('#')) {
64
+ continue;
65
+ }
66
+
67
+ if (line.startsWith('[') && line.endsWith(']')) {
68
+ inCoreSection = /^\[core\]$/i.test(line);
69
+ continue;
70
+ }
71
+
72
+ if (!inCoreSection) {
73
+ continue;
74
+ }
75
+
76
+ const match = /^hookspath\s*=\s*(.+)$/i.exec(line);
77
+ if (!match) {
78
+ continue;
79
+ }
80
+
81
+ let hooksPath = match[1]?.trim() ?? '';
82
+ if (hooksPath.startsWith('"') && hooksPath.endsWith('"')) {
83
+ hooksPath = hooksPath.slice(1, -1);
84
+ }
85
+ if (hooksPath.length === 0) {
86
+ continue;
87
+ }
88
+ return hooksPath;
89
+ }
90
+
91
+ return null;
92
+ };
93
+
94
+ export const resolvePumukiHooksDirectory = (
95
+ repoRoot: string
96
+ ): PumukiHooksDirectoryResolution => {
97
+ const gitPathHooks = resolveGitPath(repoRoot, 'hooks');
98
+ if (gitPathHooks) {
99
+ return {
100
+ path: gitPathHooks,
101
+ source: 'git-rev-parse',
102
+ };
103
+ }
104
+
105
+ const hooksPathFromConfig = readCoreHooksPathFromGitConfig(repoRoot);
106
+ if (hooksPathFromConfig) {
107
+ return {
108
+ path: isAbsolute(hooksPathFromConfig)
109
+ ? hooksPathFromConfig
110
+ : resolve(repoRoot, hooksPathFromConfig),
111
+ source: 'git-config',
112
+ };
113
+ }
114
+
115
+ return {
116
+ path: join(repoRoot, '.git', 'hooks'),
117
+ source: 'default',
118
+ };
119
+ };
120
+
37
121
  const resolveHooksDirectory = (repoRoot: string): string =>
38
- resolveGitPath(repoRoot, 'hooks') ?? join(repoRoot, '.git', 'hooks');
122
+ resolvePumukiHooksDirectory(repoRoot).path;
39
123
 
40
124
  const resolveHookPath = (repoRoot: string, hook: PumukiManagedHook): string =>
41
125
  join(resolveHooksDirectory(repoRoot), hook);
@@ -1,5 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import packageJson from '../../package.json';
2
4
 
3
- export const getCurrentPumukiVersion = (): string => packageJson.version;
5
+ const readConsumerInstalledVersion = (repoRoot: string): string | null => {
6
+ const packagePath = join(repoRoot, 'node_modules', packageJson.name, 'package.json');
7
+ if (!existsSync(packagePath)) {
8
+ return null;
9
+ }
10
+ try {
11
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf8')) as { version?: unknown };
12
+ return typeof parsed.version === 'string' && parsed.version.trim().length > 0
13
+ ? parsed.version.trim()
14
+ : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ export const getCurrentPumukiVersion = (params?: { repoRoot?: string }): string => {
21
+ const repoRoot = params?.repoRoot;
22
+ if (typeof repoRoot === 'string' && repoRoot.trim().length > 0) {
23
+ const installedVersion = readConsumerInstalledVersion(repoRoot.trim());
24
+ if (installedVersion) {
25
+ return installedVersion;
26
+ }
27
+ }
28
+ return packageJson.version;
29
+ };
4
30
 
5
31
  export const getCurrentPumukiPackageName = (): string => packageJson.name;
@@ -1,4 +1,4 @@
1
- import { getPumukiHooksStatus } from './hookManager';
1
+ import { getPumukiHooksStatus, resolvePumukiHooksDirectory } from './hookManager';
2
2
  import { LifecycleGitService, type ILifecycleGitService } from './gitService';
3
3
  import { getCurrentPumukiVersion } from './packageInfo';
4
4
  import {
@@ -12,6 +12,8 @@ export type LifecycleStatus = {
12
12
  packageVersion: string;
13
13
  lifecycleState: LifecycleState;
14
14
  hookStatus: ReturnType<typeof getPumukiHooksStatus>;
15
+ hooksDirectory: string;
16
+ hooksDirectoryResolution: 'git-rev-parse' | 'git-config' | 'default';
15
17
  trackedNodeModulesCount: number;
16
18
  policyValidation: LifecyclePolicyValidationSnapshot;
17
19
  };
@@ -23,13 +25,16 @@ export const readLifecycleStatus = (params?: {
23
25
  const git = params?.git ?? new LifecycleGitService();
24
26
  const cwd = params?.cwd ?? process.cwd();
25
27
  const repoRoot = git.resolveRepoRoot(cwd);
28
+ const hooksDirectory = resolvePumukiHooksDirectory(repoRoot);
26
29
  const trackedNodeModulesCount = git.trackedNodeModulesPaths(repoRoot).length;
27
30
 
28
31
  return {
29
32
  repoRoot,
30
- packageVersion: getCurrentPumukiVersion(),
33
+ packageVersion: getCurrentPumukiVersion({ repoRoot }),
31
34
  lifecycleState: readLifecycleState(git, repoRoot),
32
35
  hookStatus: getPumukiHooksStatus(repoRoot),
36
+ hooksDirectory: hooksDirectory.path,
37
+ hooksDirectoryResolution: hooksDirectory.source,
33
38
  trackedNodeModulesCount,
34
39
  policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
35
40
  };
@@ -0,0 +1,116 @@
1
+ import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
2
+
3
+ type AutoExecuteAction = 'proceed' | 'ask';
4
+
5
+ type AutoExecuteNextAction = {
6
+ kind: 'info' | 'run_command';
7
+ message: string;
8
+ command?: string;
9
+ };
10
+
11
+ const isEvidenceCode = (code: string): boolean =>
12
+ code === 'EVIDENCE_MISSING' || code === 'EVIDENCE_INVALID' || code === 'EVIDENCE_STALE';
13
+
14
+ const confidenceFromViolation = (violationCode: string | null): number => {
15
+ if (!violationCode) {
16
+ return 90;
17
+ }
18
+ if (isEvidenceCode(violationCode)) {
19
+ return 65;
20
+ }
21
+ if (violationCode === 'GITFLOW_PROTECTED_BRANCH') {
22
+ return 40;
23
+ }
24
+ return 50;
25
+ };
26
+
27
+ const nextActionFromViolation = (violation: AiGateViolation | undefined): AutoExecuteNextAction => {
28
+ if (!violation) {
29
+ return {
30
+ kind: 'info',
31
+ message: 'Gate listo. Puedes continuar con implementación.',
32
+ };
33
+ }
34
+ switch (violation.code) {
35
+ case 'EVIDENCE_MISSING':
36
+ case 'EVIDENCE_INVALID':
37
+ case 'EVIDENCE_STALE':
38
+ return {
39
+ kind: 'run_command',
40
+ message: 'Refresca evidencia y vuelve a evaluar PRE_WRITE.',
41
+ command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
42
+ };
43
+ case 'GITFLOW_PROTECTED_BRANCH':
44
+ return {
45
+ kind: 'run_command',
46
+ message: 'Cambia a una rama feature/* antes de continuar.',
47
+ command: 'git checkout -b feature/<descripcion-kebab-case>',
48
+ };
49
+ default:
50
+ return {
51
+ kind: 'info',
52
+ message: 'Corrige la violación bloqueante y vuelve a ejecutar auto_execute_ai_start.',
53
+ };
54
+ }
55
+ };
56
+
57
+ export type EnterpriseAutoExecuteAiStartResult = {
58
+ tool: 'auto_execute_ai_start';
59
+ dryRun: true;
60
+ executed: true;
61
+ success: boolean;
62
+ result: {
63
+ action: AutoExecuteAction;
64
+ confidence_pct: number;
65
+ reason_code: string;
66
+ next_action: AutoExecuteNextAction;
67
+ gate: {
68
+ stage: ReturnType<typeof evaluateAiGate>['stage'];
69
+ status: ReturnType<typeof evaluateAiGate>['status'];
70
+ allowed: ReturnType<typeof evaluateAiGate>['allowed'];
71
+ violations_count: number;
72
+ };
73
+ };
74
+ };
75
+
76
+ export const runEnterpriseAutoExecuteAiStart = (params: {
77
+ repoRoot: string;
78
+ stage?: AiGateStage;
79
+ requireMcpReceipt?: boolean;
80
+ }): EnterpriseAutoExecuteAiStartResult => {
81
+ const stage = params.stage ?? 'PRE_WRITE';
82
+ const evaluation = evaluateAiGate({
83
+ repoRoot: params.repoRoot,
84
+ stage,
85
+ requireMcpReceipt: params.requireMcpReceipt ?? false,
86
+ });
87
+ const firstViolation = evaluation.violations[0];
88
+ const reasonCode = firstViolation?.code ?? 'READY';
89
+ const action: AutoExecuteAction = evaluation.allowed ? 'proceed' : 'ask';
90
+ const confidencePct = confidenceFromViolation(firstViolation?.code ?? null);
91
+ const nextAction = evaluation.allowed
92
+ ? {
93
+ kind: 'info' as const,
94
+ message: 'Gate en verde. Continúa con la implementación.',
95
+ }
96
+ : nextActionFromViolation(firstViolation);
97
+
98
+ return {
99
+ tool: 'auto_execute_ai_start',
100
+ dryRun: true,
101
+ executed: true,
102
+ success: true,
103
+ result: {
104
+ action,
105
+ confidence_pct: confidencePct,
106
+ reason_code: reasonCode,
107
+ next_action: nextAction,
108
+ gate: {
109
+ stage: evaluation.stage,
110
+ status: evaluation.status,
111
+ allowed: evaluation.allowed,
112
+ violations_count: evaluation.violations.length,
113
+ },
114
+ },
115
+ };
116
+ };
@@ -9,6 +9,8 @@ import { evaluateSddPolicy, readSddStatus } from '../sdd';
9
9
  import type { SddStage } from '../sdd';
10
10
  import { toStatusPayload } from './evidencePayloads';
11
11
  import { runEnterpriseAiGateCheck } from './aiGateCheck';
12
+ import { runEnterprisePreFlightCheck } from './preFlightCheck';
13
+ import { runEnterpriseAutoExecuteAiStart } from './autoExecuteAiStart';
12
14
  import { writeMcpAiGateReceipt } from './aiGateReceipt';
13
15
 
14
16
  export interface EnterpriseServerOptions {
@@ -73,6 +75,8 @@ const ENTERPRISE_RESOURCE_DESCRIPTORS: ReadonlyArray<{
73
75
 
74
76
  const ENTERPRISE_TOOLS = [
75
77
  'ai_gate_check',
78
+ 'pre_flight_check',
79
+ 'auto_execute_ai_start',
76
80
  'check_sdd_status',
77
81
  'validate_and_fix',
78
82
  'sync_branches',
@@ -92,6 +96,18 @@ const ENTERPRISE_TOOL_DESCRIPTORS: ReadonlyArray<{
92
96
  mutating: false,
93
97
  safeByDefault: true,
94
98
  },
99
+ {
100
+ name: 'pre_flight_check',
101
+ description: 'Runs pre-flight gate checks with actionable hints using the same AI gate evaluator.',
102
+ mutating: false,
103
+ safeByDefault: true,
104
+ },
105
+ {
106
+ name: 'auto_execute_ai_start',
107
+ description: 'Returns actionable decision to continue or ask user with stable confidence and next_action.',
108
+ mutating: false,
109
+ safeByDefault: true,
110
+ },
95
111
  {
96
112
  name: 'check_sdd_status',
97
113
  description: 'Evaluates SDD/OpenSpec policy for a stage without changing repository state.',
@@ -378,6 +394,46 @@ const executeEnterpriseTool = (
378
394
  },
379
395
  };
380
396
  }
397
+ case 'pre_flight_check': {
398
+ const stage = toSddStage(args.stage, 'PRE_COMMIT');
399
+ const execution = runEnterprisePreFlightCheck({
400
+ repoRoot,
401
+ stage,
402
+ });
403
+ const receiptWrite = writeMcpAiGateReceipt({
404
+ repoRoot,
405
+ stage: execution.result.stage,
406
+ status: execution.result.status,
407
+ allowed: execution.result.allowed,
408
+ });
409
+ return {
410
+ name: toolName,
411
+ success: execution.success,
412
+ dryRun: execution.dryRun,
413
+ executed: execution.executed,
414
+ data: {
415
+ ...execution.result,
416
+ receipt: {
417
+ path: receiptWrite.path,
418
+ issued_at: receiptWrite.receipt.issued_at,
419
+ },
420
+ },
421
+ };
422
+ }
423
+ case 'auto_execute_ai_start': {
424
+ const stage = toSddStage(args.stage, 'PRE_WRITE');
425
+ const execution = runEnterpriseAutoExecuteAiStart({
426
+ repoRoot,
427
+ stage,
428
+ });
429
+ return {
430
+ name: toolName,
431
+ success: execution.success,
432
+ dryRun: execution.dryRun,
433
+ executed: execution.executed,
434
+ data: execution.result,
435
+ };
436
+ }
381
437
  case 'check_sdd_status': {
382
438
  const stage = toSddStage(args.stage, 'PRE_COMMIT');
383
439
  let evaluation: ReturnType<typeof evaluateSddPolicy>;
@@ -0,0 +1,108 @@
1
+ import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
2
+
3
+ const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
4
+ EVIDENCE_MISSING: 'Ejecuta una auditoría (1/2/3/4) para regenerar .ai_evidence.json.',
5
+ EVIDENCE_INVALID: 'Regenera .ai_evidence.json desde una opción de auditoría.',
6
+ EVIDENCE_STALE: 'Refresca evidencia antes de continuar con commit/push.',
7
+ EVIDENCE_TIMESTAMP_INVALID: 'Regenera evidencia para obtener un timestamp válido.',
8
+ EVIDENCE_GATE_BLOCKED: 'Corrige primero las violaciones bloqueantes y vuelve a auditar.',
9
+ EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este mismo repositorio.',
10
+ EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual antes de continuar.',
11
+ EVIDENCE_GATE_STATUS_INCOHERENT: 'Regenera evidencia para alinear ai_gate y severity_metrics.',
12
+ EVIDENCE_OUTCOME_INCOHERENT: 'Regenera evidencia para alinear ai_gate y snapshot.outcome.',
13
+ EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
14
+ EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH: 'Reanuda auditoría en el stage correcto.',
15
+ EVIDENCE_RULES_COVERAGE_INCOMPLETE:
16
+ 'Asegura unevaluated=0 y coverage_ratio=1 antes de continuar.',
17
+ EVIDENCE_UNSUPPORTED_AUTO_RULES:
18
+ 'Mapea todas las reglas AUTO a detectores AST antes de continuar.',
19
+ EVIDENCE_TIMESTAMP_FUTURE: 'Corrige la hora del sistema y regenera evidencia.',
20
+ GITFLOW_PROTECTED_BRANCH: 'Evita trabajo directo en ramas protegidas (usa feature/*).',
21
+ };
22
+
23
+ const buildPreFlightHints = (params: {
24
+ stage: AiGateStage;
25
+ status: ReturnType<typeof evaluateAiGate>['status'];
26
+ violations: ReadonlyArray<AiGateViolation>;
27
+ upstream: string | null;
28
+ }): ReadonlyArray<string> => {
29
+ const hints: string[] = [];
30
+ const emittedCodes = new Set<string>();
31
+ for (const violation of params.violations) {
32
+ if (emittedCodes.has(violation.code)) {
33
+ continue;
34
+ }
35
+ const action = ACTIONABLE_HINTS_BY_CODE[violation.code];
36
+ if (!action) {
37
+ continue;
38
+ }
39
+ hints.push(`${violation.code}: ${action}`);
40
+ emittedCodes.add(violation.code);
41
+ }
42
+ if (params.stage === 'PRE_PUSH' && !params.upstream) {
43
+ hints.push('PRE_PUSH sin upstream: configura tracking (git push --set-upstream origin <branch>).');
44
+ }
45
+ if (hints.length === 0) {
46
+ if (params.status === 'ALLOWED') {
47
+ hints.push('Pre-flight operativo: sin bloqueos previos detectados.');
48
+ } else {
49
+ hints.push('Corrige la causa bloqueante y vuelve a ejecutar el pre-flight.');
50
+ }
51
+ }
52
+ return hints;
53
+ };
54
+
55
+ export type EnterprisePreFlightCheckResult = {
56
+ tool: 'pre_flight_check';
57
+ dryRun: true;
58
+ executed: true;
59
+ success: boolean;
60
+ result: {
61
+ allowed: ReturnType<typeof evaluateAiGate>['allowed'];
62
+ status: ReturnType<typeof evaluateAiGate>['status'];
63
+ stage: ReturnType<typeof evaluateAiGate>['stage'];
64
+ policy: ReturnType<typeof evaluateAiGate>['policy'];
65
+ violations: ReturnType<typeof evaluateAiGate>['violations'];
66
+ evidence: ReturnType<typeof evaluateAiGate>['evidence'];
67
+ mcp_receipt: ReturnType<typeof evaluateAiGate>['mcp_receipt'];
68
+ repo_state: ReturnType<typeof evaluateAiGate>['repo_state'];
69
+ hints: ReadonlyArray<string>;
70
+ };
71
+ };
72
+
73
+ export const runEnterprisePreFlightCheck = (params: {
74
+ repoRoot: string;
75
+ stage: AiGateStage;
76
+ requireMcpReceipt?: boolean;
77
+ }): EnterprisePreFlightCheckResult => {
78
+ const evaluation = evaluateAiGate({
79
+ repoRoot: params.repoRoot,
80
+ stage: params.stage,
81
+ requireMcpReceipt: params.requireMcpReceipt ?? false,
82
+ });
83
+
84
+ const hints = buildPreFlightHints({
85
+ stage: evaluation.stage,
86
+ status: evaluation.status,
87
+ violations: evaluation.violations,
88
+ upstream: evaluation.repo_state.git.upstream,
89
+ });
90
+
91
+ return {
92
+ tool: 'pre_flight_check',
93
+ dryRun: true,
94
+ executed: true,
95
+ success: evaluation.allowed,
96
+ result: {
97
+ allowed: evaluation.allowed,
98
+ status: evaluation.status,
99
+ stage: evaluation.stage,
100
+ policy: evaluation.policy,
101
+ violations: evaluation.violations,
102
+ evidence: evaluation.evidence,
103
+ mcp_receipt: evaluation.mcp_receipt,
104
+ repo_state: evaluation.repo_state,
105
+ hints,
106
+ },
107
+ };
108
+ };
@@ -133,3 +133,31 @@ export const emitAuditSummaryNotificationFromAiGate = (
133
133
  repoRoot: params.repoRoot,
134
134
  });
135
135
  };
136
+
137
+ export const emitGateBlockedNotification = (
138
+ params: {
139
+ repoRoot: string;
140
+ stage: AuditSummaryNotificationStage;
141
+ totalViolations: number;
142
+ causeCode: string;
143
+ causeMessage: string;
144
+ remediation: string;
145
+ },
146
+ dependencies: Partial<AuditSummaryNotificationDependencies> = {}
147
+ ): SystemNotificationEmitResult => {
148
+ const activeDependencies: AuditSummaryNotificationDependencies = {
149
+ ...defaultDependencies,
150
+ ...dependencies,
151
+ };
152
+ return activeDependencies.emitSystemNotification({
153
+ event: {
154
+ kind: 'gate.blocked',
155
+ stage: params.stage,
156
+ totalViolations: params.totalViolations,
157
+ causeCode: params.causeCode,
158
+ causeMessage: params.causeMessage,
159
+ remediation: params.remediation,
160
+ },
161
+ repoRoot: params.repoRoot,
162
+ });
163
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.37",
3
+ "version": "6.3.39",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -154,10 +154,21 @@ const buildNotificationEvents = (result: AiGateCheckResult): ReadonlyArray<Pumuk
154
154
  });
155
155
  }
156
156
  if (result.status === 'BLOCKED') {
157
+ const firstViolation = result.violations[0];
158
+ const causeCode = firstViolation?.code ?? 'GATE_BLOCKED';
159
+ const causeMessage =
160
+ firstViolation?.message
161
+ ?? `Detected ${result.violations.length} blocking violations in stage ${result.stage}.`;
162
+ const remediation =
163
+ (firstViolation ? ACTIONABLE_HINTS_BY_CODE[firstViolation.code] : undefined)
164
+ ?? 'Corrige la causa bloqueante y vuelve a ejecutar el gate.';
157
165
  events.push({
158
166
  kind: 'gate.blocked',
159
167
  stage: result.stage,
160
168
  totalViolations: result.violations.length,
169
+ causeCode,
170
+ causeMessage,
171
+ remediation,
161
172
  });
162
173
  }
163
174
  return events;
@@ -234,7 +234,7 @@ export const formatEvidenceSummaryForMenu = (
234
234
  if (summary.status === 'missing') {
235
235
  return [
236
236
  'Evidence: status=missing',
237
- 'Run `npx --yes pumuki-pre-commit` to generate fresh evidence.',
237
+ 'Run `./node_modules/.bin/pumuki-pre-commit` to generate fresh evidence.',
238
238
  ].join('\n');
239
239
  }
240
240