pumuki 6.3.39 → 6.3.41

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 (64) hide show
  1. package/README.md +21 -12
  2. package/VERSION +1 -1
  3. package/core/gate/evaluateRules.test.ts +40 -0
  4. package/core/gate/evaluateRules.ts +7 -1
  5. package/core/rules/Consequence.ts +1 -0
  6. package/docs/CONFIGURATION.md +50 -0
  7. package/docs/INSTALLATION.md +38 -11
  8. package/docs/MCP_SERVERS.md +1 -1
  9. package/docs/README.md +1 -0
  10. package/docs/RELEASE_NOTES.md +58 -0
  11. package/docs/USAGE.md +191 -9
  12. package/docs/registro-maestro-de-seguimiento.md +2 -2
  13. package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1629 -1
  14. package/docs/validation/README.md +2 -1
  15. package/docs/validation/ast-intelligence-roadmap.md +96 -0
  16. package/integrations/config/skillsCustomRules.ts +14 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +11 -1
  18. package/integrations/config/skillsLock.ts +30 -0
  19. package/integrations/config/skillsMarkdownRules.ts +14 -3
  20. package/integrations/config/skillsRuleSet.ts +25 -3
  21. package/integrations/evidence/readEvidence.test.ts +3 -2
  22. package/integrations/evidence/readEvidence.ts +14 -4
  23. package/integrations/evidence/repoState.ts +10 -2
  24. package/integrations/evidence/schema.test.ts +3 -2
  25. package/integrations/evidence/schema.ts +3 -0
  26. package/integrations/evidence/writeEvidence.test.ts +3 -2
  27. package/integrations/gate/evaluateAiGate.ts +511 -2
  28. package/integrations/git/GitService.ts +5 -1
  29. package/integrations/git/astIntelligenceDualValidation.ts +275 -0
  30. package/integrations/git/gitAtomicity.ts +42 -9
  31. package/integrations/git/resolveGitRefs.ts +37 -0
  32. package/integrations/git/runPlatformGate.ts +228 -1
  33. package/integrations/git/runPlatformGateEvaluation.ts +4 -0
  34. package/integrations/git/stageRunners.ts +116 -2
  35. package/integrations/lifecycle/cli.ts +759 -22
  36. package/integrations/lifecycle/doctor.ts +62 -0
  37. package/integrations/lifecycle/index.ts +1 -0
  38. package/integrations/lifecycle/packageInfo.ts +25 -3
  39. package/integrations/lifecycle/policyReconcile.ts +304 -0
  40. package/integrations/lifecycle/preWriteAutomation.ts +42 -2
  41. package/integrations/lifecycle/watch.ts +365 -0
  42. package/integrations/mcp/aiGateCheck.ts +59 -2
  43. package/integrations/mcp/autoExecuteAiStart.ts +25 -1
  44. package/integrations/mcp/preFlightCheck.ts +13 -0
  45. package/integrations/sdd/evidenceScaffold.ts +223 -0
  46. package/integrations/sdd/index.ts +2 -0
  47. package/integrations/sdd/stateSync.ts +400 -0
  48. package/integrations/sdd/syncDocs.ts +97 -2
  49. package/package.json +4 -1
  50. package/scripts/backlog-action-reasons-lib.ts +38 -0
  51. package/scripts/backlog-id-issue-map-lib.ts +69 -0
  52. package/scripts/backlog-json-contract-lib.ts +3 -0
  53. package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
  54. package/scripts/framework-menu-system-notifications-lib.ts +66 -6
  55. package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
  56. package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
  57. package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
  58. package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
  59. package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
  60. package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
  61. package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
  62. package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
  63. package/scripts/watch-consumer-backlog-lib.ts +465 -0
  64. package/scripts/watch-consumer-backlog.ts +326 -0
@@ -13,7 +13,11 @@ import {
13
13
  resolveActiveGateWaiver,
14
14
  type GateWaiverResult,
15
15
  } from './gateWaiver';
16
- import { evaluatePlatformGateFindings } from './runPlatformGateEvaluation';
16
+ import {
17
+ evaluateAstIntelligenceDualValidation,
18
+ evaluatePlatformGateFindings,
19
+ type AstIntelligenceDualValidationResult,
20
+ } from './runPlatformGateEvaluation';
17
21
  import {
18
22
  countScannedFilesFromFacts,
19
23
  resolveFactsForGateScope,
@@ -46,6 +50,7 @@ export type GateDependencies = {
46
50
  emitPlatformGateEvidence: typeof emitPlatformGateEvidence;
47
51
  printGateFindings: typeof printGateFindings;
48
52
  enforceTddBddPolicy: typeof enforceTddBddPolicy;
53
+ evaluateAstIntelligenceDualValidation: typeof evaluateAstIntelligenceDualValidation;
49
54
  buildMemoryShadowRecommendation: (params: {
50
55
  findings: ReadonlyArray<Finding>;
51
56
  tddBddSnapshot?: TddBddSnapshot;
@@ -117,6 +122,7 @@ const defaultDependencies: GateDependencies = {
117
122
  emitPlatformGateEvidence,
118
123
  printGateFindings,
119
124
  enforceTddBddPolicy,
125
+ evaluateAstIntelligenceDualValidation,
120
126
  buildMemoryShadowRecommendation: buildDefaultMemoryShadowRecommendation,
121
127
  evaluateSddForStage: (stage, repoRoot) =>
122
128
  evaluateSddPolicy({
@@ -241,6 +247,41 @@ const isCriticalProfileSeverity = (severity: string): boolean => {
241
247
 
242
248
  const toNormalizedPath = (value: string): string => value.replace(/\\/g, '/').trim();
243
249
 
250
+ const CODE_FILE_EXTENSIONS = new Set<string>([
251
+ '.js',
252
+ '.jsx',
253
+ '.ts',
254
+ '.tsx',
255
+ '.mjs',
256
+ '.cjs',
257
+ '.swift',
258
+ '.kt',
259
+ '.kts',
260
+ ]);
261
+
262
+ const isObservedCodePath = (path: string): boolean => {
263
+ const normalized = toNormalizedPath(path).toLowerCase();
264
+ if (normalized.length === 0) {
265
+ return false;
266
+ }
267
+ if (
268
+ normalized.startsWith('apps/backend/')
269
+ || normalized.startsWith('apps/frontend/')
270
+ || normalized.startsWith('apps/web/')
271
+ || normalized.startsWith('apps/admin-dashboard/')
272
+ || normalized.startsWith('apps/ios/')
273
+ || normalized.startsWith('apps/android/')
274
+ ) {
275
+ return true;
276
+ }
277
+ for (const extension of CODE_FILE_EXTENSIONS) {
278
+ if (normalized.endsWith(extension)) {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ };
284
+
244
285
  const collectObservedPathsFromFacts = (
245
286
  facts: ReadonlyArray<Fact>
246
287
  ): ReadonlyArray<string> => {
@@ -263,6 +304,125 @@ const collectObservedPathsFromFacts = (
263
304
  return [...observedPaths].sort();
264
305
  };
265
306
 
307
+ const collectObservedCodePathsFromFacts = (
308
+ facts: ReadonlyArray<Fact>
309
+ ): ReadonlyArray<string> => {
310
+ const codePaths = collectObservedPathsFromFacts(facts).filter((path) => isObservedCodePath(path));
311
+ return [...new Set(codePaths)].sort();
312
+ };
313
+
314
+ type IosTestFileContent = {
315
+ path: string;
316
+ content: string;
317
+ };
318
+
319
+ const isIosSwiftTestPath = (path: string): boolean => {
320
+ const normalized = toNormalizedPath(path).toLowerCase();
321
+ if (!normalized.endsWith('.swift')) {
322
+ return false;
323
+ }
324
+ return normalized.includes('/tests/') || normalized.includes('/uitests/');
325
+ };
326
+
327
+ const collectIosTestFileContents = (
328
+ facts: ReadonlyArray<Fact>
329
+ ): ReadonlyArray<IosTestFileContent> => {
330
+ const filesByPath = new Map<string, string>();
331
+ for (const fact of facts) {
332
+ if (fact.kind !== 'FileContent') {
333
+ continue;
334
+ }
335
+ if (!isIosSwiftTestPath(fact.path)) {
336
+ continue;
337
+ }
338
+ const normalizedPath = toNormalizedPath(fact.path);
339
+ filesByPath.set(normalizedPath, fact.content);
340
+ }
341
+ return [...filesByPath.entries()]
342
+ .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
343
+ .map(([path, content]) => ({ path, content }));
344
+ };
345
+
346
+ const isXCTestSource = (content: string): boolean => {
347
+ return /\bimport\s+XCTest\b/.test(content) || /\bXCTestCase\b/.test(content);
348
+ };
349
+
350
+ const hasMakeSUTPattern = (content: string): boolean => /\bmakeSUT\s*\(/.test(content);
351
+
352
+ const hasTrackForMemoryLeaksPattern = (content: string): boolean =>
353
+ /\btrackForMemoryLeaks\s*\(/.test(content);
354
+
355
+ const toIosTestsQualityBlockingFinding = (params: {
356
+ stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
357
+ facts: ReadonlyArray<Fact>;
358
+ }): Finding | undefined => {
359
+ const testFiles = collectIosTestFileContents(params.facts);
360
+ if (testFiles.length === 0) {
361
+ return undefined;
362
+ }
363
+
364
+ const invalidFiles: string[] = [];
365
+ for (const testFile of testFiles) {
366
+ if (!isXCTestSource(testFile.content)) {
367
+ continue;
368
+ }
369
+ const missingMarkers: string[] = [];
370
+ if (!hasMakeSUTPattern(testFile.content)) {
371
+ missingMarkers.push('makeSUT()');
372
+ }
373
+ if (!hasTrackForMemoryLeaksPattern(testFile.content)) {
374
+ missingMarkers.push('trackForMemoryLeaks()');
375
+ }
376
+ if (missingMarkers.length === 0) {
377
+ continue;
378
+ }
379
+ invalidFiles.push(`${testFile.path}{missing=[${missingMarkers.join(', ')}]}`);
380
+ }
381
+
382
+ if (invalidFiles.length === 0) {
383
+ return undefined;
384
+ }
385
+
386
+ const sampleFiles = invalidFiles.slice(0, 3).join(' | ');
387
+ return {
388
+ ruleId: 'governance.skills.ios-test-quality.incomplete',
389
+ severity: 'ERROR',
390
+ code: 'IOS_TEST_QUALITY_PATTERN_MISSING_HIGH',
391
+ message:
392
+ `iOS test quality enforcement incomplete at ${params.stage}: ${sampleFiles}. ` +
393
+ 'Use makeSUT() factory pattern and trackForMemoryLeaks() in XCTest sources.',
394
+ filePath: '.ai_evidence.json',
395
+ matchedBy: 'IosTestsQualityGuard',
396
+ source: 'skills-ios-test-quality',
397
+ };
398
+ };
399
+
400
+ const toActiveRulesEmptyForCodeChangesBlockingFinding = (params: {
401
+ stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
402
+ facts: ReadonlyArray<Fact>;
403
+ activeRuleIds: ReadonlyArray<string>;
404
+ }): Finding | undefined => {
405
+ if (params.activeRuleIds.length > 0) {
406
+ return undefined;
407
+ }
408
+ const codePaths = collectObservedCodePathsFromFacts(params.facts);
409
+ if (codePaths.length === 0) {
410
+ return undefined;
411
+ }
412
+ const samplePaths = codePaths.slice(0, 5).join(', ');
413
+ return {
414
+ ruleId: 'governance.rules.active-rule-coverage.empty',
415
+ severity: 'ERROR',
416
+ code: 'ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH',
417
+ message:
418
+ `Active rules coverage is empty at ${params.stage} while code changes were detected. ` +
419
+ `sample_paths=[${samplePaths}]. Ensure skill/project rules are active before allowing this stage.`,
420
+ filePath: '.ai_evidence.json',
421
+ matchedBy: 'ActiveRulesCoverageGuard',
422
+ source: 'rules-coverage',
423
+ };
424
+ };
425
+
266
426
  const detectRequiredSkillsScopesFromPaths = (
267
427
  observedPaths: ReadonlyArray<string>
268
428
  ): Record<'ios' | 'android' | 'backend' | 'frontend', ReadonlyArray<string>> => {
@@ -575,6 +735,13 @@ const toCrossPlatformCriticalEnforcementBlockingFinding = (params: {
575
735
  };
576
736
  };
577
737
 
738
+ const shouldBlockFromFinding = (finding: Finding | undefined): boolean => {
739
+ if (!finding) {
740
+ return false;
741
+ }
742
+ return finding.severity === 'ERROR' || finding.severity === 'CRITICAL';
743
+ };
744
+
578
745
  export async function runPlatformGate(params: {
579
746
  policy: GatePolicy;
580
747
  auditMode?: 'gate' | 'engine';
@@ -653,6 +820,7 @@ export async function runPlatformGate(params: {
653
820
  projectRules,
654
821
  heuristicRules,
655
822
  coverage,
823
+ evaluationFacts = facts,
656
824
  findings,
657
825
  } = dependencies.evaluatePlatformGateFindings({
658
826
  facts,
@@ -728,6 +896,25 @@ export async function runPlatformGate(params: {
728
896
  evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [],
729
897
  })
730
898
  : undefined;
899
+ const activeRulesEmptyForCodeChangesFinding =
900
+ params.policy.stage === 'PRE_COMMIT' ||
901
+ params.policy.stage === 'PRE_PUSH' ||
902
+ params.policy.stage === 'CI'
903
+ ? toActiveRulesEmptyForCodeChangesBlockingFinding({
904
+ stage: params.policy.stage,
905
+ facts,
906
+ activeRuleIds: coverage?.activeRuleIds ?? [],
907
+ })
908
+ : undefined;
909
+ const iosTestsQualityFinding =
910
+ params.policy.stage === 'PRE_COMMIT' ||
911
+ params.policy.stage === 'PRE_PUSH' ||
912
+ params.policy.stage === 'CI'
913
+ ? toIosTestsQualityBlockingFinding({
914
+ stage: params.policy.stage,
915
+ facts,
916
+ })
917
+ : undefined;
731
918
  const policyAsCodeBlockingFinding =
732
919
  params.policy.stage === 'PRE_COMMIT' ||
733
920
  params.policy.stage === 'PRE_PUSH' ||
@@ -746,6 +933,33 @@ export async function runPlatformGate(params: {
746
933
  policyTrace: params.policyTrace,
747
934
  })
748
935
  : undefined;
936
+ const astIntelligenceDualValidation:
937
+ | AstIntelligenceDualValidationResult
938
+ | undefined =
939
+ params.policy.stage === 'PRE_COMMIT'
940
+ || params.policy.stage === 'PRE_PUSH'
941
+ || params.policy.stage === 'CI'
942
+ ? dependencies.evaluateAstIntelligenceDualValidation({
943
+ stage: params.policy.stage,
944
+ skillsRules: skillsRuleSet.rules,
945
+ facts: evaluationFacts,
946
+ legacyFindings: findings,
947
+ })
948
+ : undefined;
949
+ const astIntelligenceDualFinding = astIntelligenceDualValidation?.finding;
950
+ if (astIntelligenceDualValidation && astIntelligenceDualValidation.mode !== 'off') {
951
+ const summary = astIntelligenceDualValidation.summary;
952
+ process.stdout.write(
953
+ `[pumuki][ast-intelligence] mode=${astIntelligenceDualValidation.mode}` +
954
+ ` mapped_rules=${summary.mapped_rules}` +
955
+ ` compared_rules=${summary.compared_rules}` +
956
+ ` divergences=${summary.divergences}` +
957
+ ` false_positives=${summary.false_positives}` +
958
+ ` false_negatives=${summary.false_negatives}` +
959
+ ` latency_ms=${summary.latency_ms}` +
960
+ ` languages=[${summary.languages.join(',') || 'none'}]\n`
961
+ );
962
+ }
749
963
  const degradedModeBlocks = params.policyTrace?.degraded?.action === 'block';
750
964
  const rulesCoverage = coverage
751
965
  ? {
@@ -797,6 +1011,9 @@ export async function runPlatformGate(params: {
797
1011
  ...(platformSkillsCoverageFinding ? [platformSkillsCoverageFinding] : []),
798
1012
  ...(crossPlatformCriticalFinding ? [crossPlatformCriticalFinding] : []),
799
1013
  ...(skillsScopeComplianceFinding ? [skillsScopeComplianceFinding] : []),
1014
+ ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []),
1015
+ ...(iosTestsQualityFinding ? [iosTestsQualityFinding] : []),
1016
+ ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
800
1017
  ...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
801
1018
  ...tddBddEvaluation.findings,
802
1019
  ...findings,
@@ -805,6 +1022,9 @@ export async function runPlatformGate(params: {
805
1022
  || platformSkillsCoverageFinding
806
1023
  || crossPlatformCriticalFinding
807
1024
  || skillsScopeComplianceFinding
1025
+ || activeRulesEmptyForCodeChangesFinding
1026
+ || iosTestsQualityFinding
1027
+ || astIntelligenceDualFinding
808
1028
  || coverageBlockingFinding
809
1029
  || policyAsCodeBlockingFinding
810
1030
  || degradedModeFinding
@@ -816,11 +1036,15 @@ export async function runPlatformGate(params: {
816
1036
  ...(platformSkillsCoverageFinding ? [platformSkillsCoverageFinding] : []),
817
1037
  ...(crossPlatformCriticalFinding ? [crossPlatformCriticalFinding] : []),
818
1038
  ...(skillsScopeComplianceFinding ? [skillsScopeComplianceFinding] : []),
1039
+ ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []),
1040
+ ...(iosTestsQualityFinding ? [iosTestsQualityFinding] : []),
1041
+ ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
819
1042
  ...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
820
1043
  ...tddBddEvaluation.findings,
821
1044
  ...findings,
822
1045
  ]
823
1046
  : findings;
1047
+ const hasAstIntelligenceBlockingFinding = shouldBlockFromFinding(astIntelligenceDualFinding);
824
1048
  const decision = dependencies.evaluateGate([...effectiveFindings], params.policy);
825
1049
  const baseGateOutcome =
826
1050
  sddBlockingFinding ||
@@ -830,6 +1054,9 @@ export async function runPlatformGate(params: {
830
1054
  platformSkillsCoverageFinding ||
831
1055
  crossPlatformCriticalFinding ||
832
1056
  skillsScopeComplianceFinding ||
1057
+ activeRulesEmptyForCodeChangesFinding ||
1058
+ iosTestsQualityFinding ||
1059
+ hasAstIntelligenceBlockingFinding ||
833
1060
  coverageBlockingFinding ||
834
1061
  hasTddBddBlockingFinding
835
1062
  ? 'BLOCK'
@@ -20,6 +20,10 @@ import {
20
20
  import { extractHeuristicFacts } from '../../core/facts/extractHeuristicFacts';
21
21
  import { buildCombinedBaselineRules } from './baselineRuleSets';
22
22
  import { attachFindingTraceability } from './findingTraceability';
23
+ export {
24
+ evaluateAstIntelligenceDualValidation,
25
+ type AstIntelligenceDualValidationResult,
26
+ } from './astIntelligenceDualValidation';
23
27
 
24
28
  type PlatformGateEvaluationResult = {
25
29
  detectedPlatforms: DetectedPlatforms;
@@ -1,7 +1,10 @@
1
1
  import { resolvePolicyForStage } from '../gate/stagePolicies';
2
2
  import {
3
+ resolveAheadBehindFromRef,
4
+ resolveCurrentBranchRef,
3
5
  resolveCiBaseRef,
4
6
  resolvePrePushBootstrapBaseRef,
7
+ resolveUpstreamTrackingRef,
5
8
  resolveUpstreamRef,
6
9
  } from './resolveGitRefs';
7
10
  import { runPlatformGate } from './runPlatformGate';
@@ -21,6 +24,11 @@ import type { EvidenceReadResult } from '../evidence/readEvidence';
21
24
 
22
25
  const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
23
26
  'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
27
+ const PRE_PUSH_UPSTREAM_BOOTSTRAP_FALLBACK_MESSAGE =
28
+ '[pumuki][pre-push] branch has no upstream; using bootstrap range ';
29
+ const PRE_PUSH_MANUAL_FALLBACK_MESSAGE =
30
+ '[pumuki][pre-push] branch has no upstream and stdin is empty; using working-tree fallback scope.';
31
+ const PRE_PUSH_UPSTREAM_MISALIGNED_AHEAD_THRESHOLD = 5;
24
32
 
25
33
  const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
26
34
  const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
@@ -37,11 +45,16 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
37
45
  EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura coverage_ratio=1 y unevaluated=0.',
38
46
  GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
39
47
  PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>',
48
+ PRE_PUSH_UPSTREAM_MISALIGNED:
49
+ 'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
40
50
  };
41
51
 
42
52
  type StageRunnerDependencies = {
43
53
  resolvePolicyForStage: typeof resolvePolicyForStage;
44
54
  resolveUpstreamRef: typeof resolveUpstreamRef;
55
+ resolveUpstreamTrackingRef: typeof resolveUpstreamTrackingRef;
56
+ resolveCurrentBranchRef: typeof resolveCurrentBranchRef;
57
+ resolveAheadBehindFromRef: typeof resolveAheadBehindFromRef;
45
58
  resolvePrePushBootstrapBaseRef: typeof resolvePrePushBootstrapBaseRef;
46
59
  resolveCiBaseRef: typeof resolveCiBaseRef;
47
60
  runPlatformGate: typeof runPlatformGate;
@@ -75,6 +88,9 @@ type StageRunnerDependencies = {
75
88
  const defaultDependencies: StageRunnerDependencies = {
76
89
  resolvePolicyForStage,
77
90
  resolveUpstreamRef,
91
+ resolveUpstreamTrackingRef,
92
+ resolveCurrentBranchRef,
93
+ resolveAheadBehindFromRef,
78
94
  resolvePrePushBootstrapBaseRef,
79
95
  resolveCiBaseRef,
80
96
  runPlatformGate,
@@ -233,6 +249,57 @@ const shouldAllowBootstrapPrePush = (rawInput: string): boolean => {
233
249
  return false;
234
250
  };
235
251
 
252
+ const PRE_PUSH_TOPIC_BRANCH_PREFIXES = [
253
+ 'feature/',
254
+ 'bugfix/',
255
+ 'refactor/',
256
+ 'chore/',
257
+ 'docs/',
258
+ ];
259
+
260
+ const toShortRef = (value: string): string =>
261
+ value
262
+ .replace(/^refs\/heads\//, '')
263
+ .replace(/^refs\/remotes\/[^/]+\//, '')
264
+ .replace(/^[^/]+\//, '');
265
+
266
+ const detectPrePushUpstreamMisalignment = (params: {
267
+ dependencies: StageRunnerDependencies;
268
+ upstreamRef: string;
269
+ }): { message: string; remediation: string } | null => {
270
+ const currentBranch = params.dependencies.resolveCurrentBranchRef();
271
+ const upstreamTrackingRef = params.dependencies.resolveUpstreamTrackingRef();
272
+ if (!currentBranch || !upstreamTrackingRef) {
273
+ return null;
274
+ }
275
+
276
+ const isTopicBranch = PRE_PUSH_TOPIC_BRANCH_PREFIXES.some((prefix) =>
277
+ currentBranch.startsWith(prefix)
278
+ );
279
+ if (!isTopicBranch) {
280
+ return null;
281
+ }
282
+
283
+ const upstreamShort = toShortRef(upstreamTrackingRef);
284
+ if (upstreamShort !== 'main' && upstreamShort !== 'develop') {
285
+ return null;
286
+ }
287
+
288
+ const aheadBehind = params.dependencies.resolveAheadBehindFromRef(params.upstreamRef);
289
+ if (!aheadBehind || aheadBehind.ahead < PRE_PUSH_UPSTREAM_MISALIGNED_AHEAD_THRESHOLD) {
290
+ return null;
291
+ }
292
+
293
+ return {
294
+ message:
295
+ `pumuki pre-push blocked: upstream appears misaligned for ${currentBranch}. ` +
296
+ `tracking=${upstreamTrackingRef} ahead=${aheadBehind.ahead} behind=${aheadBehind.behind}.`,
297
+ remediation:
298
+ 'Alinea upstream con la rama actual para evaluar solo el delta real: ' +
299
+ 'git branch --unset-upstream && git push --set-upstream origin <branch>',
300
+ };
301
+ };
302
+
236
303
  const enforceGitAtomicityGate = (params: {
237
304
  dependencies: StageRunnerDependencies;
238
305
  repoRoot: string;
@@ -315,8 +382,19 @@ export async function runPrePushStage(
315
382
  const upstreamRef = activeDependencies.resolveUpstreamRef();
316
383
  if (!upstreamRef) {
317
384
  const prePushInput = activeDependencies.readPrePushStdin();
318
- if (shouldAllowBootstrapPrePush(prePushInput)) {
319
- const bootstrapBaseRef = activeDependencies.resolvePrePushBootstrapBaseRef();
385
+ const bootstrapBaseRef = activeDependencies.resolvePrePushBootstrapBaseRef();
386
+ const bootstrapByPrePushStdIn = shouldAllowBootstrapPrePush(prePushInput);
387
+ const bootstrapByFallbackBase = !bootstrapByPrePushStdIn && bootstrapBaseRef !== 'HEAD';
388
+ const manualInvocationFallback =
389
+ !bootstrapByPrePushStdIn &&
390
+ !bootstrapByFallbackBase &&
391
+ prePushInput.trim().length === 0;
392
+ if (bootstrapByPrePushStdIn || bootstrapByFallbackBase) {
393
+ if (bootstrapByFallbackBase) {
394
+ process.stderr.write(
395
+ `${PRE_PUSH_UPSTREAM_BOOTSTRAP_FALLBACK_MESSAGE}${bootstrapBaseRef}..HEAD\n`
396
+ );
397
+ }
320
398
  if (
321
399
  enforceGitAtomicityGate({
322
400
  dependencies: activeDependencies,
@@ -347,6 +425,25 @@ export async function runPrePushStage(
347
425
  notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
348
426
  return exitCode;
349
427
  }
428
+ if (manualInvocationFallback) {
429
+ process.stderr.write(`${PRE_PUSH_MANUAL_FALLBACK_MESSAGE}\n`);
430
+ const resolved = activeDependencies.resolvePolicyForStage('PRE_PUSH');
431
+ const exitCode = await activeDependencies.runPlatformGate({
432
+ policy: resolved.policy,
433
+ policyTrace: resolved.trace,
434
+ scope: {
435
+ kind: 'workingTree',
436
+ },
437
+ });
438
+ emitSuccessfulHookGateSummary({
439
+ dependencies: activeDependencies,
440
+ stage: 'PRE_PUSH',
441
+ policyTrace: resolved.trace,
442
+ exitCode,
443
+ });
444
+ notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
445
+ return exitCode;
446
+ }
350
447
  process.stderr.write(`${PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE}\n`);
351
448
  notifyGateBlockedForStage({
352
449
  dependencies: activeDependencies,
@@ -359,6 +456,23 @@ export async function runPrePushStage(
359
456
  return 1;
360
457
  }
361
458
 
459
+ const misalignedUpstream = detectPrePushUpstreamMisalignment({
460
+ dependencies: activeDependencies,
461
+ upstreamRef,
462
+ });
463
+ if (misalignedUpstream) {
464
+ process.stderr.write(`${misalignedUpstream.message}\n`);
465
+ notifyGateBlockedForStage({
466
+ dependencies: activeDependencies,
467
+ stage: 'PRE_PUSH',
468
+ fallbackCauseCode: 'PRE_PUSH_UPSTREAM_MISALIGNED',
469
+ fallbackCauseMessage: misalignedUpstream.message,
470
+ fallbackRemediation: misalignedUpstream.remediation,
471
+ });
472
+ notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
473
+ return 1;
474
+ }
475
+
362
476
  if (
363
477
  enforceGitAtomicityGate({
364
478
  dependencies: activeDependencies,